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" } }