From eab872ff09822a368017a612ce99e4e7c1ddc178 Mon Sep 17 00:00:00 2001 From: LukeMackin Date: Sun, 19 Jul 2026 10:42:17 +0800 Subject: [PATCH] =?UTF-8?q?build(android):=20=E7=94=9F=E6=88=90=E5=8E=9F?= =?UTF-8?q?=E7=94=9FAndroid=E5=B7=A5=E7=A8=8B=20=E2=80=94=20Android=20Stud?= =?UTF-8?q?io=E7=9B=B4=E6=8E=A5=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - android/: 完整Gradle工程(build.gradle/settings/AndroidManifest/Activity/Application) - PackageInstallerModule: 重写为ReactContextBaseJavaModule (bare RN, 移除Expo依赖) - 新增sha256File原生方法, installApk改为Promise异步 - 移除旧的modules/ (Expo Module → bare React Native) - 添加eas.json (备用云端构建) --- .../client-android/android/app/build.gradle | 38 ++++++++ .../android/app/src/main/AndroidManifest.xml | 28 ++++++ .../main/java/dev/yuzu/gca/MainActivity.kt | 18 ++++ .../main/java/dev/yuzu/gca/MainApplication.kt | 40 ++++++++ .../dev/yuzu/gca/PackageInstallerModule.kt | 92 +++++++++++++++++++ .../dev/yuzu/gca/PackageInstallerPackage.kt | 16 ++++ .../app/src/main/res/values/colors.xml | 5 + .../app/src/main/res/values/strings.xml | 4 + packages/client-android/android/build.gradle | 26 ++++++ .../client-android/android/gradle.properties | 5 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../client-android/android/settings.gradle | 6 ++ packages/client-android/eas.json | 23 +++++ 13 files changed, 306 insertions(+) create mode 100644 packages/client-android/android/app/build.gradle create mode 100644 packages/client-android/android/app/src/main/AndroidManifest.xml create mode 100644 packages/client-android/android/app/src/main/java/dev/yuzu/gca/MainActivity.kt create mode 100644 packages/client-android/android/app/src/main/java/dev/yuzu/gca/MainApplication.kt create mode 100644 packages/client-android/android/app/src/main/java/dev/yuzu/gca/PackageInstallerModule.kt create mode 100644 packages/client-android/android/app/src/main/java/dev/yuzu/gca/PackageInstallerPackage.kt create mode 100644 packages/client-android/android/app/src/main/res/values/colors.xml create mode 100644 packages/client-android/android/app/src/main/res/values/strings.xml create mode 100644 packages/client-android/android/build.gradle create mode 100644 packages/client-android/android/gradle.properties create mode 100644 packages/client-android/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/client-android/android/settings.gradle create mode 100644 packages/client-android/eas.json 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": {} + } +}