37 Commits

Author SHA1 Message Date
LukeMackin
73d7eff9d9 merge: 同步main的清理 2026-07-20 12:57:23 +08:00
LukeMackin
1684fac260 chore: 清理OCR临时文件+截图+调试脚本,更新.gitignore 2026-07-20 12:57:14 +08:00
LukeMackin
6cd2a22d09 feat(expo): app.json 添加 runtimeVersion 字段 2026-07-20 00:38:32 +08:00
LukeMackin
71f6b6369b fix: expo-manifest.json 改为 Expo Updates Modern Manifest 格式(id+createdAt+runtimeVersion) 2026-07-19 23:46:17 +08:00
LukeMackin
e5ce52ac40 feat(expo): JS热更新(expo-updates) + APK大更新双轨
- app.json: expo-updates配置(checkAutomatically ON_LOAD)
- ota/expo-manifest.json: JS bundle热更新manifest
- App.tsx: 启动时自动检查JS+APK双轨更新
- pnpm install后npx expo prebuild生成android/原生工程
2026-07-19 23:40:17 +08:00
LukeMackin
a2f204f5ee docs: gen-manifest.sh构建号规则说明 2026-07-19 23:23:23 +08:00
LukeMackin
eeea966400 fix: gitignore路径修复 + 版本比较改用PackageInfo动态获取 2026-07-19 23:16:12 +08:00
LukeMackin
d68e6ca6df feat: 版本比较 — 远程与本地一致则跳过更新 2026-07-19 23:12:44 +08:00
LukeMackin
a96560966c fix: 空渠道检测改用JSONObject.has + 移除gitignore误排除manifest 2026-07-19 23:10:03 +08:00
LukeMackin
ffd05e3e17 feat: 三渠道分级 - nightly=当前版本, beta/stable留空(暂无更新) 2026-07-19 23:05:11 +08:00
LukeMackin
40d0c86292 build: versionCode 7→8 + Release #8 + APK 3.5MB 2026-07-19 22:57:30 +08:00
LukeMackin
e8f00faf41 feat: App内显示版本号 v0.1.0-dav-android-7-debug 2026-07-19 21:27:06 +08:00
LukeMackin
b70c5015a0 feat: 改用系统安装器弹窗(FileProvider+ACTION_VIEW), 无需手动找权限 2026-07-19 21:06:04 +08:00
LukeMackin
14367a229b fix: 权限跳转改为应用详情页(精确定位) 2026-07-19 21:01:36 +08:00
LukeMackin
e02997db52 feat: 安装权限不足时自动跳转系统设置页面 2026-07-19 20:55:55 +08:00
LukeMackin
82a0650b59 fix: manifest SHA256更新(当前APK重编后哈希变化) 2026-07-19 20:47:35 +08:00
LukeMackin
0d3fa632e6 feat: appendLog同步输出到android.util.Log(GCA-OTA tag) 2026-07-19 20:42:06 +08:00
LukeMackin
ad1b3e3aca fix: PackageInstaller session管理 — openSession失败abandon + commit失败abandon 2026-07-19 20:00:46 +08:00
LukeMackin
0f37573faf feat: OTA校验通过后调用PackageInstaller真正安装APK
- SHA256通过→installApk()→PackageInstaller Session API
- 安装完成后通知栏提示重启
- 权限检查: canRequestPackageInstalls()
2026-07-19 19:57:56 +08:00
LukeMackin
49d3c187d0 fix: gen-manifest.sh注释同步manifest-stable.json 2026-07-19 19:51:46 +08:00
LukeMackin
3da606671a fix: gen-manifest.sh curl路径+注释+README全部同步manifest-stable.json 2026-07-19 19:48:24 +08:00
LukeMackin
1461acd292 fix: gen-manifest.sh/README/gitignore同步manifest-stable.json命名 2026-07-19 19:46:27 +08:00
LukeMackin
a7b6b455d1 fix: stable渠道manifest文件名修复 manifest.json→manifest-stable.json 2026-07-19 19:41:22 +08:00
LukeMackin
c62e2a4430 feat: ota/三渠道manifest(固定raw URL)
- ota/manifest-stable.json / beta.json / nightly.json
- App URL改回raw(固定路径,不随版本号变)
- v0.1.0→v0.2.0升级时只需更新JSON内容,URL不变
2026-07-19 19:37:04 +08:00
LukeMackin
20d81d5ab8 chore: ota/manifest.json加入gitignore(构建产物) 2026-07-19 19:29:17 +08:00
LukeMackin
c1849802b6 feat: ota/gen-manifest.sh — CI构建时动态生成manifest.json
- gen-manifest.sh: 计算APK SHA256 → 填入版本号/URL → 生成manifest.json
- manifest.json本身不提交仓库(构建产物,上传到Release)
- README.md说明目录用途
2026-07-19 19:28:58 +08:00
LukeMackin
31a56708d6 refactor: manifest回归Release构建产物
- ota/目录删除(manifest是CI构建时动态生成上传)
- App URL改为Release下载路径(releases/download/...)
- 流程: CI build→生成manifest(SHA256动态)→上传Release
- Release #7: v0.1.0-dav-android-7-debug.apk + manifest.json
2026-07-19 15:58:24 +08:00
LukeMackin
1b3d2fc06f fix: APK命名加buildType后缀 + manifest URL同步
- v0.1.0-dav-android-7.apk → v0.1.0-dav-android-7-debug.apk
- 3个manifest URL同步更新
2026-07-19 15:57:24 +08:00
LukeMackin
db5b7de50f fix: APK命名补充构建次数 vX.Y.Z-dav-{client}-{build}.apk
- archivesBaseName移除,改用applicationVariants动态命名
- versionCode: 6→7
- APK: v0.1.0-dav-android-7.apk
2026-07-19 15:54:34 +08:00
LukeMackin
3244876c8c fix(security): usesCleartextTraffic移除 + 域名白名单 + JSONObject解析
- AndroidManifest: 移除 usesCleartextTraffic=true
- downloadFile: 增加 ALLOWED_HOSTS 白名单(git.childish-ghost.com/github.com)
- manifest解析: indexOf/substring → org.json.JSONObject
2026-07-19 15:52:56 +08:00
LukeMackin
fcc51f4356 refactor: manifest迁移到ota/文件夹
- ota/manifest.json (stable)
- ota/manifest-beta.json
- ota/manifest-nightly.json
- MainActivity URL更新为ota/路径
2026-07-19 15:47:26 +08:00
LukeMackin
70e39454df fix: beta/nightly manifest tag格式修正为v0.1.0-dav-android-7(匹配版本正则) 2026-07-19 15:45:41 +08:00
LukeMackin
d4169538c9 fix: manifest放回仓库源码 + APK按版本号命名
- manifest.json/beta/nightly 从Release Assets移到源码仓库
- APK命名: gca-v0.1.0 → v0.1.0-dav-android-debug.apk
- Release #7: 仅含APK(prerelease), manifest从raw URL拉取
- 三渠道manifest: manifest.json / manifest-beta.json / manifest-nightly.json
2026-07-19 15:42:01 +08:00
LukeMackin
a3f0f975a6 chore: review notes + cleanup 2026-07-19 15:21:43 +08:00
LukeMackin
109ad813e6 fix(android): APK包名统一gca-v0.1.0 + 应用名加版本 + Gitea manifest联调
- APK: app-debug.apk → gca-v0.1.0-debug.apk
- App名: Yuzu GCA Test → Yuzu GCA v0.1.0
- MainActivity: 真实manifest拉取+JSON解析+SHA256真实比对
- Gitea Release #6: gca-v0.1.0.apk + manifest.json
- 删除旧Expo modules/ 文件
2026-07-19 15:18:30 +08:00
LukeMackin
34cdec130f build(android): APK构建成功 — 纯原生OTA测试App
- 下载Gradle Wrapper 8.10.2 (43KB)
- MainActivity: 自包含OTA测试(manifest拉取/下载/SHA256/PackageInstaller)
- 移除React Native依赖, 轻量原生App仅3.2MB
- local.properties指向本机Android SDK
- BUILD SUCCESSFUL → app-debug.apk (3.2MB)
2026-07-19 10:55:51 +08:00
LukeMackin
eab872ff09 build(android): 生成原生Android工程 — Android Studio直接构建
- android/: 完整Gradle工程(build.gradle/settings/AndroidManifest/Activity/Application)
- PackageInstallerModule: 重写为ReactContextBaseJavaModule (bare RN, 移除Expo依赖)
- 新增sha256File原生方法, installApk改为Promise异步
- 移除旧的modules/ (Expo Module → bare React Native)
- 添加eas.json (备用云端构建)
2026-07-19 10:42:17 +08:00
44 changed files with 538 additions and 110 deletions

11
.gitignore vendored
View File

@@ -39,9 +39,18 @@ src-tauri/target/
# Android # Android
android/app/build/ android/app/build/
android/.gradle/ **/android/.gradle/
android/build/ android/build/
*.hprof *.hprof
*.jks *.jks
*.keystore *.keystore
local.properties local.properties
screen.png
# Temp/debug files
*.traineddata
tessdata_temp/
*.ps1
*_temp/

View File

@@ -0,0 +1,8 @@
{
"topic_20260718-152740_2cab7b15ea20700d": {
"stage": 3,
"userTurns": 3,
"basisHash": "942c58e1a7d2413a",
"updatedAt": 1784389797592
}
}

View File

@@ -0,0 +1,3 @@
{
"topic_20260718-152740_2cab7b15ea20700d": 1784388460704
}

View File

@@ -0,0 +1,3 @@
{
"topic_20260718-152740_2cab7b15ea20700d": "auto"
}

View File

@@ -0,0 +1,3 @@
{
"topic_20260718-152740_2cab7b15ea20700d": "Start pursuing the…"
}

18
ota/README.md Normal file
View File

@@ -0,0 +1,18 @@
# OTA 文件夹
此目录存放 manifest 文件。manifest-stable.json 是 CI 构建时动态生成的产物,
也提交到仓库通过 raw URL 供客户端拉取。
## 文件
- `gen-manifest.sh` — 构建后运行,计算 APK SHA256 并生成 manifest-stable.json
- `manifest-stable.json` — stable 渠道
- `manifest-beta.json` — beta 渠道
- `manifest-nightly.json` — nightly 渠道
## 流程
```
CI 构建 APK → ./ota/gen-manifest.sh → git push manifest-stable.json
手机 App → raw URL 拉取 manifest → 比较版本 → 下载 APK → SHA256 校验
```

12
ota/expo-manifest.json Normal file
View File

@@ -0,0 +1,12 @@
{
"id": "e5ce52a-0000-0000-0000-000000000000",
"createdAt": "2026-07-19T15:30:00.000Z",
"runtimeVersion": "0.1.0",
"launchAsset": {
"key": "bundle",
"contentType": "application/javascript",
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-8/bundle.js"
},
"assets": [],
"metadata": {}
}

38
ota/gen-manifest.sh Normal file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# OTA manifest 生成脚本
# 用法: ./ota/gen-manifest.sh <versionName> <versionCode> <apk-path>
# 构建号规则: 每次生成APK时 versionCode +1, 对应 git tag vX.Y.Z-dav-android-N
# CI 构建时自动运行, SHA256动态填入, 生成 manifest-stable.json 后上传到 Gitea Release
VERSION_NAME="${1:-0.1.0}"
VERSION_CODE="${2:-7}"
APK_PATH="${3:-packages/client-android/android/app/build/outputs/apk/debug/v0.1.0-dav-android-7-debug.apk}"
RELEASE_TAG="v${VERSION_NAME}-dav-android-${VERSION_CODE}"
APK_NAME="v${VERSION_NAME}-dav-android-${VERSION_CODE}-debug.apk"
SHA256=$(sha256sum "$APK_PATH" | cut -d' ' -f1)
SIZE=$(stat -c%s "$APK_PATH")
cat > ota/manifest-stable.json <<JSON
{
"version": "${VERSION_NAME}",
"builds": {
"android": {
"type": "apk",
"tag": "${RELEASE_TAG}",
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/${RELEASE_TAG}/${APK_NAME}",
"sha256": "${SHA256}",
"size": ${SIZE},
"channel": "stable",
"minVersion": "${VERSION_NAME}"
}
}
}
JSON
echo "Generated: ota/manifest-stable.json"
echo " tag: ${RELEASE_TAG}"
echo " sha256: ${SHA256}"
echo " size: ${SIZE}"
echo ""
echo "Upload: curl -X POST 'https://git.childish-ghost.com/api/v1/repos/LukeMackin/Yuzu-GCA/releases/\${RELEASE_ID}/assets?name=manifest-stable.json' -H 'Authorization: token \$TOKEN' -H 'Content-Type: application/json' --data-binary @ota/manifest-stable.json"

1
ota/manifest-beta.json Normal file
View File

@@ -0,0 +1 @@
{"version":"0.1.0","builds":{}}

View File

@@ -0,0 +1 @@
{"version":"0.1.0","builds":{"android":{"type":"apk","tag":"v0.1.0-dav-android-8","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-8/v0.1.0-dav-android-8-debug.apk","sha256":"b2451a398ea2dd34e585c5ec4529d6654451c9b9ab4ddf4a25ec9b032f0ac2bf","size":3506569,"channel":"nightly","minVersion":"0.1.0"}}}

1
ota/manifest-stable.json Normal file
View File

@@ -0,0 +1 @@
{"version":"0.1.0","builds":{}}

View File

@@ -0,0 +1,2 @@
#Sun Jul 19 10:50:39 CST 2026
gradle.version=8.10.2

View File

@@ -0,0 +1,40 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
android {
namespace "dev.yuzu.gca"
compileSdk 35
defaultConfig {
applicationId "dev.yuzu.gca"
minSdk 24
targetSdk 35
versionCode 8
versionName "0.1.0"
}
buildTypes {
release { minifyEnabled false }
debug { debuggable true }
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = "17" }
applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "v${variant.versionName}-dav-android-${variant.versionCode}-${variant.buildType.name}.apk"
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.0.21"
implementation "androidx.appcompat:appcompat:1.7.0"
implementation "androidx.core:core:1.15.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
}

View File

@@ -0,0 +1,33 @@
<?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: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>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,255 @@
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.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"
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"
textSize = 20f
setPadding(0, 0, 0, 4)
}
layout.addView(title)
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)
}
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) {
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) }
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#3b82f6</color>
<color name="background">#0f172a</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Yuzu GCA v0.1.0</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="apk" path="/" />
</paths>

View 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' }
}
}

View File

@@ -0,0 +1,5 @@
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
android.useAndroidX=true
android.enableJetifier=true
newArchEnabled=true
hermesEnabled=true

Binary file not shown.

View 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

View 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

View File

@@ -0,0 +1,3 @@
rootProject.name = 'YuzuGCA'
include ':app'

View File

@@ -3,40 +3,26 @@
"name": "YuzuGCA", "name": "YuzuGCA",
"slug": "yuzu-gca", "slug": "yuzu-gca",
"version": "0.1.0", "version": "0.1.0",
"runtimeVersion": "0.1.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "dark", "userInterfaceStyle": "dark",
"newArchEnabled": true, "newArchEnabled": true,
"splash": { "splash": { "backgroundColor": "#0f172a" },
"backgroundColor": "#0f172a" "ios": { "supportsTablet": false, "bundleIdentifier": "dev.yuzu.gca" },
},
"ios": {
"supportsTablet": false,
"bundleIdentifier": "dev.yuzu.gca"
},
"android": { "android": {
"adaptiveIcon": { "adaptiveIcon": { "backgroundColor": "#0f172a" },
"backgroundColor": "#0f172a"
},
"package": "dev.yuzu.gca", "package": "dev.yuzu.gca",
"allowBackup": false, "allowBackup": false,
"permissions": [ "versionCode": 8,
"REQUEST_INSTALL_PACKAGES" "permissions": ["REQUEST_INSTALL_PACKAGES"]
]
}, },
"plugins": [ "plugins": ["expo-updates"],
"expo-updates"
],
"updates": { "updates": {
"enabled": true, "enabled": true,
"checkAutomatically": "ON_LOAD", "checkAutomatically": "ON_LOAD",
"fallbackToCacheTimeout": 3000, "fallbackToCacheTimeout": 3000,
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/manifest/expo-manifest.json" "url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/expo-manifest.json"
}, },
"extra": { "extra": { "eas": { "projectId": "yuzu-gca-android" } }
"eas": {
"projectId": "yuzu-gca-android"
}
}
} }
} }

View 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": {}
}
}

View File

@@ -1,13 +0,0 @@
{
"version": "0.1.0",
"builds": {
"android": {
"type": "apk",
"tag": "v0.1.0-dav-android-1",
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-1/gca-0.1.0.apk",
"sha256": "PLACEHOLDER",
"size": 0,
"minVersion": "0.1.0"
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -1,4 +0,0 @@
{
"platform": "android",
"modules": ["PackageInstallerModule"]
}