build: 代理构建成功 — Gradle代理+恢复file_paths+Expo SDK53
This commit is contained in:
@@ -1,61 +1,255 @@
|
||||
package dev.yuzu.gca
|
||||
|
||||
import android.os.Build
|
||||
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.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
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var logView: TextView
|
||||
private var selectedChannel = "stable"
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val scroll = ScrollView(this)
|
||||
val layout = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(24, 24, 24, 24)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
val title = TextView(this).apply {
|
||||
text = "Yuzu GCA"
|
||||
textSize = 20f
|
||||
setPadding(0, 0, 0, 4)
|
||||
}
|
||||
layout.addView(title)
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this,
|
||||
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||
object : DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName,
|
||||
fabricEnabled
|
||||
){})
|
||||
val versionInfo = TextView(this).apply {
|
||||
text = "v0.1.0-dav-android-8-debug"
|
||||
textSize = 12f
|
||||
setTextColor(Color.GRAY)
|
||||
setPadding(0, 0, 0, 12)
|
||||
}
|
||||
layout.addView(versionInfo)
|
||||
|
||||
val channelLabel = TextView(this).apply {
|
||||
text = "选择更新渠道:"
|
||||
textSize = 13f
|
||||
setPadding(0, 8, 0, 4)
|
||||
}
|
||||
layout.addView(channelLabel)
|
||||
|
||||
// 渠道按钮行
|
||||
val channelRow = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
setPadding(0, 0, 0, 12)
|
||||
}
|
||||
|
||||
/**
|
||||
* Align the back button behavior with Android S
|
||||
* where moving root activities to background instead of finishing activities.
|
||||
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
|
||||
*/
|
||||
override fun invokeDefaultOnBackPressed() {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
if (!moveTaskToBack(false)) {
|
||||
// For non-root activities, use the default implementation to finish them.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
fun makeChannelBtn(label: String, channel: String): Button {
|
||||
return Button(this).apply {
|
||||
text = label
|
||||
textSize = 12f
|
||||
setOnClickListener {
|
||||
selectedChannel = channel
|
||||
highlightChannel(channelRow, this)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use the default back button implementation on Android S
|
||||
// because it's doing more than [Activity.moveTaskToBack] in fact.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
val btnStable = makeChannelBtn("Stable", "stable")
|
||||
val btnBeta = makeChannelBtn("Beta", "beta")
|
||||
val btnNightly = makeChannelBtn("Nightly", "nightly")
|
||||
|
||||
channelRow.addView(btnStable)
|
||||
channelRow.addView(btnBeta)
|
||||
channelRow.addView(btnNightly)
|
||||
layout.addView(channelRow)
|
||||
highlightChannel(channelRow, btnStable)
|
||||
|
||||
logView = TextView(this).apply {
|
||||
textSize = 12f
|
||||
setTextColor(Color.LTGRAY)
|
||||
}
|
||||
layout.addView(logView)
|
||||
|
||||
val btn = Button(this).apply {
|
||||
text = "检查更新"
|
||||
setOnClickListener {
|
||||
val ch = selectedChannel
|
||||
CoroutineScope(Dispatchers.IO).launch { doOtaTest(ch) }
|
||||
}
|
||||
}
|
||||
layout.addView(btn)
|
||||
|
||||
scroll.addView(layout)
|
||||
setContentView(scroll)
|
||||
|
||||
appendLog("渠道: stable | 点击[检查更新]开始")
|
||||
}
|
||||
|
||||
private fun highlightChannel(row: LinearLayout, active: Button) {
|
||||
for (i in 0 until row.childCount) {
|
||||
val b = row.getChildAt(i) as Button
|
||||
b.isSelected = (b == active)
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendLog(msg: String) {
|
||||
Log.i(TAG, msg)
|
||||
runOnUiThread { logView.append("$msg\n") }
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="apk" path="/" />
|
||||
</paths>
|
||||
@@ -54,3 +54,7 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
||||
|
||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||
expo.useLegacyPackaging=false
|
||||
systemProp.http.proxyHost=127.0.0.1
|
||||
systemProp.http.proxyPort=10808
|
||||
systemProp.https.proxyHost=127.0.0.1
|
||||
systemProp.https.proxyPort=10808
|
||||
|
||||
Reference in New Issue
Block a user