diff --git a/ota/h5/index-v6.html b/ota/h5/index-v6.html
index 3bb67b1..45a38d0 100644
--- a/ota/h5/index-v6.html
+++ b/ota/h5/index-v6.html
@@ -1,25 +1,33 @@
-Yuzu GCA v6
-version 0.1.0-dav-android-6
+
+ Yuzu GCA v6
+ 热更新 v6 — 渠道选择器
+
-
-
Stable
-
Beta
-
Nightly
+
+
更新渠道
+
+
Stable
+
Beta
+
Nightly
+
@@ -31,33 +39,28 @@ var BASE='https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota
function selectCh(ch){
channel=ch;
- document.querySelectorAll('.chip').forEach(c=>c.className='chip chip-off');
+ var chips=document.querySelectorAll('.chip');
+ for(var i=0;i
{if(!r.ok)throw Error('HTTP '+r.status);return r.text()})
.then(t=>{t=t.trim();log('remote: '+t);
- var local=document.getElementById('ver').innerText;
- if(t!==local&&t!=='v'+local&&'v'+t!==local){dlVer=t;dlUrl=BASE+'/h5/index-v7.html';
+ var local='0.1.0-dav-android-6';
+ if(t!==local&&t!=='0.1.0-dav-android-6'){dlVer=t;dlUrl=BASE+'/h5/index-v6.html';
log('*** NEW VERSION ***');document.getElementById('btnApply').disabled=false}
else log('up to date')})
.catch(e=>log('ERR: '+e.message))
}
-
function applyUpdate(){
log('downloading...');
fetch(dlUrl+'?t='+Date.now(),{cache:'no-store'})
.then(r=>{if(!r.ok)throw Error('HTTP '+r.status);return r.text()})
- .then(t=>{localStorage.setItem('gca_html',t);localStorage.setItem('gca_ver',dlVer);
- log('OK reloading...');setTimeout(function(){location.reload()},300)})
+ .then(function(t){Android.saveCache(t);log('OK! restart app to apply')})
.catch(e=>log('ERR: '+e.message))
}
diff --git a/packages/client-android/android/app/src/main/assets/index.html b/packages/client-android/android/app/src/main/assets/index.html
index 427682e..f3f26dc 100644
--- a/packages/client-android/android/app/src/main/assets/index.html
+++ b/packages/client-android/android/app/src/main/assets/index.html
@@ -1,14 +1,5 @@
-
+
Yuzu GCA
version v0.1.0-dav-android-5
@@ -44,8 +34,8 @@ function applyUpdate(){
log('downloading...');
fetch(dlUrl+'?t='+Date.now(),{cache:'no-store'})
.then(r=>{if(!r.ok)throw Error('HTTP '+r.status);return r.text()})
- .then(t=>{localStorage.setItem('gca_html',t);localStorage.setItem('gca_ver',dlVer);
- log('OK reloading...');setTimeout(function(){location.reload()},300)})
+ .then(t=>{Android.saveCache(t);log('OK! restart app to apply');
+ document.getElementById('btnApply').disabled=true})
.catch(e=>log('ERR: '+e.message))
}
diff --git a/packages/client-android/android/app/src/main/java/dev/yuzu/gca/MainActivity.kt b/packages/client-android/android/app/src/main/java/dev/yuzu/gca/MainActivity.kt
index 94a3e33..8b56d12 100644
--- a/packages/client-android/android/app/src/main/java/dev/yuzu/gca/MainActivity.kt
+++ b/packages/client-android/android/app/src/main/java/dev/yuzu/gca/MainActivity.kt
@@ -1,32 +1,15 @@
package dev.yuzu.gca
-import android.app.PendingIntent
-import android.content.Intent
-import android.content.pm.PackageInstaller
-import android.graphics.Color
-import android.net.Uri
import android.os.Bundle
-import android.provider.Settings
+import android.util.Log
import android.webkit.WebView
import android.webkit.WebViewClient
-import android.widget.Button
-import android.widget.LinearLayout
-import android.widget.ScrollView
-import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
-import kotlinx.coroutines.*
import java.io.File
-import java.io.FileOutputStream
-import java.net.HttpURLConnection
-import java.net.URL
-import java.security.MessageDigest
-import android.util.Log
-import androidx.core.content.FileProvider
-import org.json.JSONObject
class MainActivity : AppCompatActivity() {
- private lateinit var logView: TextView
- private var selectedChannel = "stable"
+
+ private val cacheFile: File by lazy { File(filesDir, "gca_hot.html") }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -39,167 +22,32 @@ class MainActivity : AppCompatActivity() {
settings.allowFileAccessFromFileURLs = true
settings.allowUniversalAccessFromFileURLs = true
settings.mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
- webViewClient = object : WebViewClient() {
- override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
- if (url.startsWith("http")) { view.loadUrl(url); return true }
- return false
- }
+ addJavascriptInterface(Bridge(this@MainActivity), "Android")
+
+ if (cacheFile.exists()) {
+ val html = cacheFile.readText()
+ loadDataWithBaseURL("https://gca.local/", html, "text/html", "UTF-8", null)
+ Log.i(TAG, "Loaded cached hot-update HTML (${html.length}B)")
+ } else {
+ loadUrl("file:///android_asset/index.html")
+ Log.i(TAG, "Loaded embedded index.html")
}
- addJavascriptInterface(AndroidBridge(this@MainActivity), "Android")
- loadUrl("file:///android_asset/index.html")
}
setContentView(wv)
}
- inner class AndroidBridge(private val ctx: MainActivity) {
+ inner class Bridge(private val ctx: MainActivity) {
@android.webkit.JavascriptInterface
- fun checkUpdate(channel: String) {
- CoroutineScope(Dispatchers.IO).launch { doOtaTest(channel) }
+ fun saveCache(html: String) {
+ ctx.cacheFile.writeText(html)
+ Log.i(TAG, "Hot-update HTML saved (${html.length}B)")
}
@android.webkit.JavascriptInterface
- fun log(msg: String) { appendLog(msg) }
- }
-
- private fun appendLog(msg: String) {
- Log.i(TAG, msg)
- if (::logView.isInitialized) {
- runOnUiThread { logView.append("$msg\n") }
+ fun clearCache() {
+ ctx.cacheFile.delete()
+ Log.i(TAG, "Cache cleared")
}
}
- companion object { private const val TAG = "GCA-OTA" }
-
- private fun manifestUrl(channel: String): String {
- val base = "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota"
- return when (channel) {
- "beta" -> "$base/manifest-beta.json"
- "nightly" -> "$base/manifest-nightly.json"
- else -> "$base/manifest-stable.json"
- }
- }
-
- private suspend fun doOtaTest(channel: String) = withContext(Dispatchers.IO) {
- val url = manifestUrl(channel)
- appendLog("=== OTA 渠道: $channel ===")
- appendLog("manifest: $url")
-
- // 1. 拉取 manifest
- appendLog("[1/4] 拉取 manifest...")
- val manifestJson: String
- try {
- manifestJson = httpGet(url)
- appendLog(" ✅ 获取成功 (${manifestJson.length}B)")
- } catch (e: Exception) {
- appendLog(" ❌ 获取失败: ${e.message}")
- appendLog("=== 测试中止 ===")
- return@withContext
- }
-
- // 2. 解析
- appendLog("[2/4] 解析 manifest...")
- var manifestSha256 = ""
- var manifestApkUrl = ""
- var manifestTag = ""
- try {
- val json = JSONObject(manifestJson)
- if (!json.getJSONObject("builds").has("android")) {
- appendLog(" ℹ️ 该渠道暂无可用更新")
- appendLog("=== $channel 渠道测试完成 ===")
- return@withContext
- }
- val android = json.getJSONObject("builds").getJSONObject("android")
- manifestSha256 = android.getString("sha256")
- manifestApkUrl = android.getString("url")
- manifestTag = android.getString("tag")
- appendLog(" 渠道: $channel 版本: $manifestTag")
- appendLog(" SHA256: ${manifestSha256.take(16)}...")
-
- // 版本比较:一致则跳过
- val localTag = "v0.1.0-dav-android-${packageManager.getPackageInfo(packageName, 0).versionCode}"
- if (manifestTag == localTag) {
- appendLog(" ✅ 已是最新版本,无需更新")
- appendLog("=== $channel 渠道测试完成 ===")
- return@withContext
- }
- appendLog(" 发现新版本,开始更新...")
- } catch (e: Exception) {
- appendLog(" ❌ 解析失败: ${e.message}")
- return@withContext
- }
-
- // 3. 下载
- appendLog("[3/4] 下载 APK...")
- val apkFile = File(cacheDir, "update-$channel.apk")
- try {
- downloadFile(manifestApkUrl, apkFile)
- appendLog(" ✅ 下载完成: ${apkFile.length()} bytes")
- } catch (e: Exception) {
- appendLog(" ❌ 下载失败: ${e.message}")
- return@withContext
- }
-
- // 4. SHA256 校验
- appendLog("[4/4] SHA256 校验...")
- val actualSha256 = computeSha256(apkFile)
- if (actualSha256 == manifestSha256) {
- appendLog(" ✅ SHA256 校验通过!")
- // 5. 安装 APK
- appendLog("[5/5] 提交安装...")
- try {
- installApk(apkFile)
- appendLog(" ✅ 安装已提交,稍后通知栏点击重启")
- } catch (e: Exception) {
- appendLog(" ❌ 安装失败: ${e.message}")
- }
- } else {
- appendLog(" ❌ SHA256 校验失败!")
- appendLog(" expected: ${manifestSha256.take(32)}")
- appendLog(" actual: ${actualSha256.take(32)}")
- }
-
- appendLog("=== $channel 渠道测试完成 ===")
- }
-
- private fun httpGet(url: String): String {
- val conn = URL(url).openConnection() as HttpURLConnection
- conn.connectTimeout = 10000
- conn.readTimeout = 10000
- return conn.inputStream.bufferedReader().readText()
- }
-
- private val ALLOWED_HOSTS = setOf("git.childish-ghost.com", "github.com")
-
- private fun downloadFile(url: String, dest: File) {
- val host = URL(url).host
- if (!ALLOWED_HOSTS.any { host == it || host.endsWith(".$it") }) {
- throw SecurityException("不允许的下载域名: $host")
- }
- val conn = URL(url).openConnection() as HttpURLConnection
- conn.connectTimeout = 30000
- conn.readTimeout = 120000
- conn.inputStream.use { input ->
- FileOutputStream(dest).use { output -> input.copyTo(output) }
- }
- }
-
- private fun installApk(apkFile: File) {
- val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", apkFile)
- val intent = Intent(Intent.ACTION_VIEW).apply {
- setDataAndType(uri, "application/vnd.android.package-archive")
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
- }
- startActivity(intent)
- }
-
- private fun computeSha256(file: File): String {
- val digest = MessageDigest.getInstance("SHA-256")
- file.inputStream().use { input ->
- val buf = ByteArray(8192)
- var read: Int
- while (input.read(buf).also { read = it } != -1) {
- digest.update(buf, 0, read)
- }
- }
- return digest.digest().joinToString("") { "%02x".format(it) }
- }
+ companion object { private const val TAG = "GCA-H5" }
}