diff --git a/packages/client-android/android/app/build.gradle b/packages/client-android/android/app/build.gradle
new file mode 100644
index 0000000..8da84e8
--- /dev/null
+++ b/packages/client-android/android/app/build.gradle
@@ -0,0 +1,38 @@
+apply plugin: "com.android.application"
+apply plugin: "org.jetbrains.kotlin.android"
+
+android {
+ namespace "dev.yuzu.gca"
+ compileSdk rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ applicationId "dev.yuzu.gca"
+ minSdk rootProject.ext.minSdkVersion
+ targetSdk rootProject.ext.targetSdkVersion
+ versionCode 4
+ 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("com.facebook.react:react-android")
+ implementation("com.facebook.react:hermes-android")
+}
diff --git a/packages/client-android/android/app/src/main/AndroidManifest.xml b/packages/client-android/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..335b475
--- /dev/null
+++ b/packages/client-android/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 0000000..3f9bb53
--- /dev/null
+++ b/packages/client-android/android/app/src/main/java/dev/yuzu/gca/MainActivity.kt
@@ -0,0 +1,18 @@
+package dev.yuzu.gca
+
+import android.os.Bundle
+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 : ReactActivity() {
+ override fun getMainComponentName(): String = "main"
+
+ override fun createReactActivityDelegate(): ReactActivityDelegate =
+ DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(null)
+ }
+}
diff --git a/packages/client-android/android/app/src/main/java/dev/yuzu/gca/MainApplication.kt b/packages/client-android/android/app/src/main/java/dev/yuzu/gca/MainApplication.kt
new file mode 100644
index 0000000..9626378
--- /dev/null
+++ b/packages/client-android/android/app/src/main/java/dev/yuzu/gca/MainApplication.kt
@@ -0,0 +1,40 @@
+package dev.yuzu.gca
+
+import android.app.Application
+import com.facebook.react.PackageList
+import com.facebook.react.ReactApplication
+import com.facebook.react.ReactHost
+import com.facebook.react.ReactNativeHost
+import com.facebook.react.ReactPackage
+import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
+import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
+import com.facebook.react.defaults.DefaultReactNativeHost
+import com.facebook.soloader.SoLoader
+
+class MainApplication : Application(), ReactApplication {
+
+ override val reactNativeHost: ReactNativeHost =
+ object : DefaultReactNativeHost(this) {
+ override fun getPackages(): List =
+ PackageList(this).packages.apply {
+ // 注册 PackageInstaller 原生模块
+ add(PackageInstallerPackage())
+ }
+
+ override fun getJSMainModuleName(): String = "index"
+
+ override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
+
+ override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
+ override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
+ }
+
+ override val reactHost: ReactHost
+ get() = getDefaultReactHost(applicationContext, reactNativeHost)
+
+ override fun onCreate() {
+ super.onCreate()
+ SoLoader.init(this, false)
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) load()
+ }
+}
diff --git a/packages/client-android/android/app/src/main/java/dev/yuzu/gca/PackageInstallerModule.kt b/packages/client-android/android/app/src/main/java/dev/yuzu/gca/PackageInstallerModule.kt
new file mode 100644
index 0000000..2d5bee9
--- /dev/null
+++ b/packages/client-android/android/app/src/main/java/dev/yuzu/gca/PackageInstallerModule.kt
@@ -0,0 +1,92 @@
+package dev.yuzu.gca
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.pm.PackageInstaller
+import com.facebook.react.bridge.*
+import java.io.File
+import java.io.FileInputStream
+import java.security.DigestInputStream
+import java.security.MessageDigest
+
+class PackageInstallerModule : ReactContextBaseJavaModule {
+ constructor(reactContext: ReactApplicationContext) : super(reactContext)
+
+ override fun getName(): String = "PackageInstallerModule"
+
+ @ReactMethod
+ fun installApk(filePath: String, expectedSha256: String, promise: Promise) {
+ try {
+ val context = reactApplicationContext
+ val apkFile = File(filePath)
+ if (!apkFile.exists()) {
+ promise.reject("FILE_NOT_FOUND", "安装包不存在")
+ return
+ }
+
+ if (!context.packageManager.canRequestPackageInstalls()) {
+ promise.reject("PERMISSION_DENIED", "REQUEST_INSTALL_PACKAGES 权限未授予")
+ return
+ }
+
+ 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 {
+ 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) {
+ promise.reject("SHA256_MISMATCH", "SHA256校验失败")
+ return
+ }
+
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
+ ?: run {
+ promise.reject("NO_LAUNCH_INTENT", "无法获取启动Intent")
+ return
+ }
+ val pendingIntent = PendingIntent.getActivity(
+ context, 0, launchIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ session.commit(pendingIntent.intentSender)
+ promise.resolve("安装已提交")
+ } finally {
+ session.close()
+ }
+ } catch (e: Exception) {
+ promise.reject("INSTALL_ERROR", e.message)
+ }
+ }
+
+ @ReactMethod
+ fun sha256File(filePath: String, promise: Promise) {
+ try {
+ val digest = MessageDigest.getInstance("SHA-256")
+ FileInputStream(File(filePath)).use { input ->
+ val buf = ByteArray(8192)
+ var read: Int
+ while (input.read(buf).also { read = it } != -1) {
+ digest.update(buf, 0, read)
+ }
+ }
+ promise.resolve(digest.digest().joinToString("") { "%02x".format(it) })
+ } catch (e: Exception) {
+ promise.reject("SHA256_ERROR", e.message)
+ }
+ }
+}
diff --git a/packages/client-android/android/app/src/main/java/dev/yuzu/gca/PackageInstallerPackage.kt b/packages/client-android/android/app/src/main/java/dev/yuzu/gca/PackageInstallerPackage.kt
new file mode 100644
index 0000000..72146a0
--- /dev/null
+++ b/packages/client-android/android/app/src/main/java/dev/yuzu/gca/PackageInstallerPackage.kt
@@ -0,0 +1,16 @@
+package dev.yuzu.gca
+
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ViewManager
+
+class PackageInstallerPackage : ReactPackage {
+ override fun createNativeModules(reactContext: ReactApplicationContext): List {
+ return listOf(PackageInstallerModule())
+ }
+
+ override fun createViewManagers(reactContext: ReactApplicationContext): List> {
+ return emptyList()
+ }
+}
diff --git a/packages/client-android/android/app/src/main/res/values/colors.xml b/packages/client-android/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..0e358ad
--- /dev/null
+++ b/packages/client-android/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #3b82f6
+ #0f172a
+
diff --git a/packages/client-android/android/app/src/main/res/values/strings.xml b/packages/client-android/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..997f9bd
--- /dev/null
+++ b/packages/client-android/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Yuzu GCA
+
diff --git a/packages/client-android/android/build.gradle b/packages/client-android/android/build.gradle
new file mode 100644
index 0000000..0e6996d
--- /dev/null
+++ b/packages/client-android/android/build.gradle
@@ -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' }
+ }
+}
diff --git a/packages/client-android/android/gradle.properties b/packages/client-android/android/gradle.properties
new file mode 100644
index 0000000..3bccec9
--- /dev/null
+++ b/packages/client-android/android/gradle.properties
@@ -0,0 +1,5 @@
+org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
+android.useAndroidX=true
+android.enableJetifier=true
+newArchEnabled=true
+hermesEnabled=true
diff --git a/packages/client-android/android/gradle/wrapper/gradle-wrapper.properties b/packages/client-android/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..18330fc
--- /dev/null
+++ b/packages/client-android/android/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/packages/client-android/android/settings.gradle b/packages/client-android/android/settings.gradle
new file mode 100644
index 0000000..62b4fdb
--- /dev/null
+++ b/packages/client-android/android/settings.gradle
@@ -0,0 +1,6 @@
+rootProject.name = 'YuzuGCA'
+
+apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle")
+applyNativeModulesSettingsGradle(settings)
+
+include ':app'
diff --git a/packages/client-android/eas.json b/packages/client-android/eas.json
new file mode 100644
index 0000000..ca64b4e
--- /dev/null
+++ b/packages/client-android/eas.json
@@ -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": {}
+ }
+}