Compare commits
5 Commits
v0.1.0-dav
...
d4169538c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4169538c9 | ||
|
|
a3f0f975a6 | ||
|
|
109ad813e6 | ||
|
|
34cdec130f | ||
|
|
eab872ff09 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -45,3 +45,4 @@ android/build/
|
|||||||
*.jks
|
*.jks
|
||||||
*.keystore
|
*.keystore
|
||||||
local.properties
|
local.properties
|
||||||
|
screen.png
|
||||||
|
|||||||
8
.reasonix/desktop-topic-auto-title-meta.json
Normal file
8
.reasonix/desktop-topic-auto-title-meta.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"topic_20260718-152740_2cab7b15ea20700d": {
|
||||||
|
"stage": 3,
|
||||||
|
"userTurns": 3,
|
||||||
|
"basisHash": "942c58e1a7d2413a",
|
||||||
|
"updatedAt": 1784389797592
|
||||||
|
}
|
||||||
|
}
|
||||||
3
.reasonix/desktop-topic-created-at.json
Normal file
3
.reasonix/desktop-topic-created-at.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260718-152740_2cab7b15ea20700d": 1784388460704
|
||||||
|
}
|
||||||
3
.reasonix/desktop-topic-title-sources.json
Normal file
3
.reasonix/desktop-topic-title-sources.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260718-152740_2cab7b15ea20700d": "auto"
|
||||||
|
}
|
||||||
3
.reasonix/desktop-topic-titles.json
Normal file
3
.reasonix/desktop-topic-titles.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260718-152740_2cab7b15ea20700d": "Start pursuing the…"
|
||||||
|
}
|
||||||
BIN
packages/client-android/android/.gradle/8.10.2/checksums/checksums.lock
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/checksums/checksums.lock
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/checksums/md5-checksums.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/checksums/md5-checksums.bin
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/checksums/sha1-checksums.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/checksums/sha1-checksums.bin
vendored
Normal file
Binary file not shown.
0
packages/client-android/android/.gradle/8.10.2/dependencies-accessors/gc.properties
vendored
Normal file
0
packages/client-android/android/.gradle/8.10.2/dependencies-accessors/gc.properties
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/executionHistory/executionHistory.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/executionHistory/executionHistory.bin
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/executionHistory/executionHistory.lock
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/executionHistory/executionHistory.lock
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/fileChanges/last-build.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileChanges/last-build.bin
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/fileHashes.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/fileHashes.bin
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/fileHashes.lock
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/fileHashes.lock
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/resourceHashesCache.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/resourceHashesCache.bin
vendored
Normal file
Binary file not shown.
0
packages/client-android/android/.gradle/8.10.2/gc.properties
vendored
Normal file
0
packages/client-android/android/.gradle/8.10.2/gc.properties
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,2 @@
|
|||||||
|
#Sun Jul 19 10:50:39 CST 2026
|
||||||
|
gradle.version=8.10.2
|
||||||
Binary file not shown.
BIN
packages/client-android/android/.gradle/file-system.probe
Normal file
BIN
packages/client-android/android/.gradle/file-system.probe
Normal file
Binary file not shown.
35
packages/client-android/android/app/build.gradle
Normal file
35
packages/client-android/android/app/build.gradle
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
apply plugin: "com.android.application"
|
||||||
|
apply plugin: "org.jetbrains.kotlin.android"
|
||||||
|
|
||||||
|
archivesBaseName = "v0.1.0-dav-android"
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "dev.yuzu.gca"
|
||||||
|
compileSdk 35
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "dev.yuzu.gca"
|
||||||
|
minSdk 24
|
||||||
|
targetSdk 35
|
||||||
|
versionCode 6
|
||||||
|
versionName "0.1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release { minifyEnabled false }
|
||||||
|
debug { debuggable true }
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions { jvmTarget = "17" }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.0.21"
|
||||||
|
implementation "androidx.appcompat:appcompat:1.7.0"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:label="Yuzu GCA v0.1.0"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
android:theme="@style/Theme.AppCompat.DayNight">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="Yuzu GCA v0.1.0"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package dev.yuzu.gca
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
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
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private lateinit var logView: TextView
|
||||||
|
private var selectedChannel = "stable"
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val scroll = ScrollView(this)
|
||||||
|
val layout = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(24, 24, 24, 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
val title = TextView(this).apply {
|
||||||
|
text = "Yuzu GCA v0.1.0"
|
||||||
|
textSize = 20f
|
||||||
|
setPadding(0, 0, 0, 8)
|
||||||
|
}
|
||||||
|
layout.addView(title)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeChannelBtn(label: String, channel: String): Button {
|
||||||
|
return Button(this).apply {
|
||||||
|
text = label
|
||||||
|
textSize = 12f
|
||||||
|
setOnClickListener {
|
||||||
|
selectedChannel = channel
|
||||||
|
highlightChannel(channelRow, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
runOnUiThread { logView.append("$msg\n") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun manifestUrl(channel: String): String {
|
||||||
|
val base = "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/packages/client-android"
|
||||||
|
return when (channel) {
|
||||||
|
"beta" -> "$base/manifest-beta.json"
|
||||||
|
"nightly" -> "$base/manifest-nightly.json"
|
||||||
|
else -> "$base/manifest.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 {
|
||||||
|
fun extractJson(key: String): String {
|
||||||
|
val field = "\"$key\": \""
|
||||||
|
val start = manifestJson.indexOf(field) + field.length
|
||||||
|
val end = manifestJson.indexOf("\"", start)
|
||||||
|
return manifestJson.substring(start, end)
|
||||||
|
}
|
||||||
|
manifestSha256 = extractJson("sha256")
|
||||||
|
manifestApkUrl = extractJson("url")
|
||||||
|
manifestTag = extractJson("tag")
|
||||||
|
appendLog(" 渠道: $channel 版本: $manifestTag")
|
||||||
|
appendLog(" SHA256: ${manifestSha256.take(16)}...")
|
||||||
|
} 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 校验通过!")
|
||||||
|
} 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 fun downloadFile(url: String, dest: File) {
|
||||||
|
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 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,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="primary">#3b82f6</color>
|
||||||
|
<color name="background">#0f172a</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Yuzu GCA v0.1.0</string>
|
||||||
|
</resources>
|
||||||
26
packages/client-android/android/build.gradle
Normal file
26
packages/client-android/android/build.gradle
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Top-level build file
|
||||||
|
buildscript {
|
||||||
|
ext {
|
||||||
|
buildToolsVersion = "35.0.0"
|
||||||
|
minSdkVersion = 24
|
||||||
|
compileSdkVersion = 35
|
||||||
|
targetSdkVersion = 35
|
||||||
|
kotlinVersion = "2.0.21"
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.7.3")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://www.jitpack.io' }
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/client-android/android/gradle.properties
Normal file
5
packages/client-android/android/gradle.properties
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
newArchEnabled=true
|
||||||
|
hermesEnabled=true
|
||||||
BIN
packages/client-android/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
packages/client-android/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
packages/client-android/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
packages/client-android/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
26
packages/client-android/android/gradlew.bat
vendored
Normal file
26
packages/client-android/android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
set ANDROID_HOME=C:\Users\Middl\AppData\Local\Android\Sdk
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
goto fail
|
||||||
|
:fail
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
exit /b 1
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
goto fail
|
||||||
|
:execute
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
:end
|
||||||
3
packages/client-android/android/settings.gradle
Normal file
3
packages/client-android/android/settings.gradle
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
rootProject.name = 'YuzuGCA'
|
||||||
|
|
||||||
|
include ':app'
|
||||||
23
packages/client-android/eas.json
Normal file
23
packages/client-android/eas.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"version": ">= 14.0.0"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"preview": {
|
||||||
|
"android": {
|
||||||
|
"buildType": "apk",
|
||||||
|
"env": {
|
||||||
|
"APP_VERSION": "0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"android": {
|
||||||
|
"buildType": "apk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/client-android/manifest-beta.json
Normal file
1
packages/client-android/manifest-beta.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":"0.1.0","builds":{"android":{"type":"apk","tag":"v0.1.0-dav-android-7-beta","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-7/v0.1.0-dav-android-debug.apk","sha256":"6cd6bbfe07e8b86b06bc328b7b08138dfb00defc71c8bcc98fbb256b2153aba6","size":3223411,"channel":"beta"}}}
|
||||||
1
packages/client-android/manifest-nightly.json
Normal file
1
packages/client-android/manifest-nightly.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":"0.1.0","builds":{"android":{"type":"apk","tag":"v0.1.0-dav-android-7-nightly","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-7/v0.1.0-dav-android-debug.apk","sha256":"6cd6bbfe07e8b86b06bc328b7b08138dfb00defc71c8bcc98fbb256b2153aba6","size":3223411,"channel":"nightly"}}}
|
||||||
@@ -3,10 +3,11 @@
|
|||||||
"builds": {
|
"builds": {
|
||||||
"android": {
|
"android": {
|
||||||
"type": "apk",
|
"type": "apk",
|
||||||
"tag": "v0.1.0-dav-android-1",
|
"tag": "v0.1.0-dav-android-7",
|
||||||
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-1/gca-0.1.0.apk",
|
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-7/v0.1.0-dav-android-debug.apk",
|
||||||
"sha256": "PLACEHOLDER",
|
"sha256": "6cd6bbfe07e8b86b06bc328b7b08138dfb00defc71c8bcc98fbb256b2153aba6",
|
||||||
"size": 0,
|
"size": 3223411,
|
||||||
|
"channel": "stable",
|
||||||
"minVersion": "0.1.0"
|
"minVersion": "0.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
package dev.yuzu.gca
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import expo.modules.kotlin.modules.Module
|
|
||||||
import expo.modules.kotlin.modules.ModuleDefinition
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.security.DigestInputStream
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
class PackageInstallerModule : Module() {
|
|
||||||
override fun definition() = ModuleDefinition {
|
|
||||||
Name("PackageInstallerModule")
|
|
||||||
|
|
||||||
AsyncFunction("installApk") { filePath: String, expectedSha256: String ->
|
|
||||||
installApk(filePath, expectedSha256)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun installApk(filePath: String, expectedSha256: String) {
|
|
||||||
val context: Context = appContext.reactContext ?: return
|
|
||||||
val apkFile = File(filePath)
|
|
||||||
if (!apkFile.exists()) throw Exception("安装包不存在")
|
|
||||||
|
|
||||||
if (!context.packageManager.canRequestPackageInstalls()) {
|
|
||||||
throw SecurityException("REQUEST_INSTALL_PACKAGES 权限未授予")
|
|
||||||
}
|
|
||||||
|
|
||||||
val packageInstaller = context.packageManager.packageInstaller
|
|
||||||
val sessionParams = PackageInstaller.SessionParams(
|
|
||||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
|
||||||
)
|
|
||||||
|
|
||||||
val sessionId = packageInstaller.createSession(sessionParams)
|
|
||||||
val session = packageInstaller.openSession(sessionId)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 边写入边计算 SHA256(原子化,避免 TOCTOU)
|
|
||||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
|
||||||
FileInputStream(apkFile).use { input ->
|
|
||||||
val digestStream = DigestInputStream(input, sha256)
|
|
||||||
session.openWrite("package", 0, apkFile.length()).use { output ->
|
|
||||||
digestStream.copyTo(output)
|
|
||||||
session.fsync(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val actualSha256 = sha256.digest().joinToString("") { "%02x".format(it) }
|
|
||||||
if (actualSha256 != expectedSha256) {
|
|
||||||
throw SecurityException("SHA256校验失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
||||||
?: throw Exception("无法获取启动Intent")
|
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
|
||||||
context, 0, launchIntent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
session.commit(pendingIntent.intentSender)
|
|
||||||
} finally {
|
|
||||||
session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"platform": "android",
|
|
||||||
"modules": ["PackageInstallerModule"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user