diff --git a/.gitignore b/.gitignore index 6f8d58c..5342e7d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ local.properties /.idea/ /.kotlin/ /framework/build/ +/framework/src/main/rust/**/target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0917ef7..4e795bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `FwMediaBrowserService` strategy and wired it into config, startup, shutdown, and health checks. - Added native startActivity strategies for `NEW_TASK + EXCLUDE_FROM_RECENTS + NO_ANIMATION` and `moveTaskToFront`. - Added `FwStart.startAuditAll()` for full native strategy audit while keeping `FwStart.start()` on executable strategies by default. +- Added optional Rust Native skeleton build with 4 Android ABI outputs behind `-PfwBuildRust=true`. +- Added Rust read-only process diagnostics for OOM adj, memory, process status, root marker, and process count probing. +- Added optional Rust MediaRoute native state and heartbeat implementation with C++ fallback. ### Changed - Hardened VPN startup to require user authorization, foreground notification startup, no default traffic routing, and explicit special-use subtype metadata. diff --git a/README-en.md b/README-en.md index 59a9a48..4a88acb 100644 --- a/README-en.md +++ b/README-en.md @@ -222,6 +222,14 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk # Publish framework artifact to local Maven ./gradlew :framework:publishReleasePublicationToMavenLocal +# Optional Rust Native skeleton +rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android +./gradlew :framework:checkFwRustToolchain +./gradlew :framework:assembleRelease -PfwBuildRust=true +# If cargo is not in PATH, append: -PfwCargoPath=/path/to/cargo + +The optional Rust layer currently provides JNI dynamic registration, build-pipeline probing, read-only process diagnostics, and MediaRoute state/heartbeat logic. Existing `libfw_native.so` and `libfw_mediaroute.so` behavior remains available as fallback. + # Repeated kill/recovery test ./kill_alive.sh diff --git a/README-ja.md b/README-ja.md index 302ee33..27b16e4 100644 --- a/README-ja.md +++ b/README-ja.md @@ -106,6 +106,10 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk ./gradlew build ./gradlew :framework:publishReleasePublicationToMavenLocal +rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android +./gradlew :framework:checkFwRustToolchain +./gradlew :framework:assembleRelease -PfwBuildRust=true + ./kill_alive.sh adb logcat | grep -E "(Fw|FwStart|FwHealth|ServiceStarter)" ``` diff --git a/README-ko.md b/README-ko.md index 39b6c94..1312393 100644 --- a/README-ko.md +++ b/README-ko.md @@ -106,6 +106,10 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk ./gradlew build ./gradlew :framework:publishReleasePublicationToMavenLocal +rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android +./gradlew :framework:checkFwRustToolchain +./gradlew :framework:assembleRelease -PfwBuildRust=true + ./kill_alive.sh adb logcat | grep -E "(Fw|FwStart|FwHealth|ServiceStarter)" ``` diff --git a/README-zh-Hant.md b/README-zh-Hant.md index 8d4bf85..de35013 100644 --- a/README-zh-Hant.md +++ b/README-zh-Hant.md @@ -106,6 +106,10 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk ./gradlew build ./gradlew :framework:publishReleasePublicationToMavenLocal +rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android +./gradlew :framework:checkFwRustToolchain +./gradlew :framework:assembleRelease -PfwBuildRust=true + ./kill_alive.sh adb logcat | grep -E "(Fw|FwStart|FwHealth|ServiceStarter)" ``` diff --git a/README.md b/README.md index adcab9f..102b386 100644 --- a/README.md +++ b/README.md @@ -406,6 +406,20 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk # 输出:release/app-202604101118.apk + mapping 文件 ``` +### Rust Native(可选) + +源码工程默认不强制构建 Rust so,避免没有 Rust 工具链的环境无法编译现有 C++ 版本。需要验证 Rust 迁移骨架时,先安装 Rust 并添加 Android targets,然后显式开启构建开关: + +```bash +rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android +./gradlew :framework:checkFwRustToolchain +./gradlew :framework:assembleRelease -PfwBuildRust=true +``` + +如果 `cargo` 不在 `PATH` 中,可以追加 `-PfwCargoPath=/path/to/cargo` 指定 Cargo 路径。 + +开启后会为 `armeabi-v7a`、`arm64-v8a`、`x86`、`x86_64` 生成并打包 `libfw_rust.so`。当前 Rust 层已接入 JNI 动态注册、构建链路探测、只读进程信息快照和 MediaRoute 状态/心跳逻辑;现有 `libfw_native.so` 与 `libfw_mediaroute.so` 行为保持不变,Rust so 不存在时自动回退。 + ### 保活测试 ```bash diff --git a/app/src/androidTest/java/com/google/services/FwStartInstrumentedTest.kt b/app/src/androidTest/java/com/google/services/FwStartInstrumentedTest.kt new file mode 100644 index 0000000..d177222 --- /dev/null +++ b/app/src/androidTest/java/com/google/services/FwStartInstrumentedTest.kt @@ -0,0 +1,208 @@ +/** + * ============================================================================ + * FwStartInstrumentedTest.kt - Native startActivity 设备侧真实触发测试 + * ============================================================================ + * + * 功能简介: + * 在 Android 设备或模拟器上真实调用 C++ start 文件夹的统一入口, + * 覆盖 application context、Activity context、只登记策略和异常入参。 + * + * 函数简介: + * - startWithZeroMask_returnsInvalidArgument:验证空策略受控失败。 + * - applicationContextStrategies_triggerNativeStart:验证应用上下文可执行策略。 + * - activityContextStrategies_triggerNativeStart:验证 Activity 上下文可执行策略。 + * - systemConstrainedStrategies_returnControlledResult:验证系统限制策略受控返回。 + * - allStartStrategies_areCoveredByInstrumentedTests:验证所有策略均被测试覆盖。 + * + * @author Pangu-Immortal + * @github https://github.com/Pangu-Immortal/KeepLiveService + * @since 2.0.1 + */ +package com.google.services + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.service.framework.start.FwStart +import com.service.framework.start.FwStartResult +import com.service.framework.start.FwStartStrategy +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Native startActivity 设备侧真实触发测试。 + */ +@RunWith(AndroidJUnit4::class) +class FwStartInstrumentedTest { + + // application context 下稳定成功的公开策略。 + private val applicationStableStrategies = listOf( + FwStartStrategy.CONTEXT_NEW_TASK, + FwStartStrategy.CONTEXT_NEW_TASK_EXCLUDE_RECENTS, + FwStartStrategy.DOUBLE_START_ACTIVITIES + ) + + // Activity context 下稳定成功的公开策略。 + private val activityStableStrategies = listOf( + FwStartStrategy.CONTEXT_DIRECT, + FwStartStrategy.START_FOR_RESULT_HOOK + ) + + // 受 Android 版本、权限、前后台状态或安全策略限制的策略。 + private val systemConstrainedStrategies = listOf( + FwStartStrategy.PENDING_INTENT_SEND, + FwStartStrategy.BINDER_START_ACTIVITIES, + FwStartStrategy.VIRTUAL_DISPLAY, + FwStartStrategy.START_NEXT_MATCHING, + FwStartStrategy.MOVE_TASK_TO_FRONT, + FwStartStrategy.NOTIFICATION_BAL, + FwStartStrategy.MEDIA_BUTTON_BAL, + FwStartStrategy.CREDENTIAL_MANAGER, + FwStartStrategy.PRINT_MANAGER, + FwStartStrategy.SHELL_START_IN_VSYNC + ) + + // 当前测试启动的 Activity,结束时统一关闭。 + private var launchedActivity: Activity? = null + + /** + * 测试结束后关闭由用例启动的 Activity。 + */ + @After + fun finishLaunchedActivity() { + // 避免多次 startActivity 后影响后续用例焦点。 + launchedActivity?.finish() + // 清空 Activity 引用。 + launchedActivity = null + } + + /** + * 获取被测应用上下文。 + */ + private fun targetContext(): Context { + // targetContext 指向 app 模块真实安装包。 + return InstrumentationRegistry.getInstrumentation().targetContext + } + + /** + * 构造指向 debug 测试 Activity 的真实 Intent。 + */ + private fun mainIntent(context: Context): Intent { + // 使用字符串类名,避免 androidTest 编译期依赖 debug 源集类。 + return Intent().setClassName(context.packageName, "com.google.services.TestStartActivity") + } + + /** + * 启动一个真实 debug 测试 Activity,并返回 Activity context。 + */ + private fun launchMainActivity(): Activity { + // 使用 instrumentation 在目标进程真实启动 Activity。 + val instrumentation = InstrumentationRegistry.getInstrumentation() + // Activity 启动需要 NEW_TASK 标志。 + val intent = mainIntent(targetContext()).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // 同步等待 Activity 创建完成。 + val activity = instrumentation.startActivitySync(intent) + // 保存 Activity,便于测试结束关闭。 + launchedActivity = activity + // 等待主线程空闲,确保窗口和 taskId 就绪。 + instrumentation.waitForIdleSync() + return activity + } + + /** + * 验证 modeMask=0 时不会进入 Native 执行,并返回参数错误。 + */ + @Test + fun startWithZeroMask_returnsInvalidArgument() { + // 获取真实应用上下文。 + val context = targetContext() + // 执行空策略启动。 + val result = FwStart.startWithMask(context, mainIntent(context), 0) + // 空策略必须返回参数错误。 + assertEquals("空策略应返回参数错误", FwStartResult.INVALID_ARGUMENT, result.nativeCode) + } + + /** + * 验证 application context 下的公开可执行 startActivity 策略能真实触发。 + */ + @Test + fun applicationContextStrategies_triggerNativeStart() { + // 获取真实应用上下文。 + val context = targetContext() + + // 逐个策略单独触发,确保真实打到每个 Native 分支。 + for (strategy in applicationStableStrategies) { + // 每次使用新 Intent,避免前一个策略修改 flags 后影响后一个策略。 + val result = FwStart.startWithStrategies(context, mainIntent(context), listOf(strategy)) + // 公开 application context 策略应命中对应策略。 + assertEquals("${strategy.displayName} 应成功命中", strategy, result.strategy) + // 成功结果必须携带正数 Native 策略位。 + assertTrue("${strategy.displayName} 返回码应为正数", result.nativeCode > 0) + } + } + + /** + * 验证 Activity context 下的公开可执行 startActivity 策略能真实触发或受控失败。 + */ + @Test + fun activityContextStrategies_triggerNativeStart() { + // 启动真实 Activity context。 + val activity = launchMainActivity() + + // 逐个策略单独触发,确保真实打到每个 Native 分支。 + for (strategy in activityStableStrategies) { + // 每次使用 Activity 构造新 Intent。 + val result = FwStart.startWithStrategies(activity, mainIntent(activity), listOf(strategy)) + // Activity 直启和 startActivityForResult 属于公开稳定 API,应成功命中。 + assertEquals("${strategy.displayName} 应成功命中", strategy, result.strategy) + // 成功结果必须携带正数 Native 策略位。 + assertTrue("${strategy.displayName} 返回码应为正数", result.nativeCode > 0) + } + } + + /** + * 验证受系统限制的策略会真实进入 Native 分支,并以成功或受控失败结束。 + */ + @Test + fun systemConstrainedStrategies_returnControlledResult() { + // 启动真实 Activity context,供需要 Activity 的系统策略使用。 + val activity = launchMainActivity() + + // 逐个策略单独触发,确保真实打到每个 Native 分支。 + for (strategy in systemConstrainedStrategies) { + // Activity 相关策略使用 Activity context,其余策略使用 application context。 + val context = when (strategy) { + FwStartStrategy.START_NEXT_MATCHING, + FwStartStrategy.MOVE_TASK_TO_FRONT -> activity + else -> targetContext() + } + // 每次使用新 Intent,避免策略修改 flags 后影响下一个策略。 + val result = FwStart.startWithStrategies(context, mainIntent(context), listOf(strategy)) + // 这些策略必须真实进入 Native,不能被 Kotlin 层空策略拦截。 + assertNotEquals("${strategy.displayName} 不应返回参数错误", FwStartResult.INVALID_ARGUMENT, result.nativeCode) + // 系统限制策略允许成功或受控失败,但必须有明确返回码。 + assertTrue("${strategy.displayName} 应返回明确结果", result.success || result.nativeCode < 0) + } + } + + /** + * 验证当前枚举中的每一个 startActivity 策略都被设备侧测试覆盖。 + */ + @Test + fun allStartStrategies_areCoveredByInstrumentedTests() { + // 汇总所有测试矩阵中声明覆盖的策略。 + val coveredStrategies = ( + applicationStableStrategies + + activityStableStrategies + + systemConstrainedStrategies + ).toSet() + // 当前枚举中的所有策略必须都在测试矩阵中。 + assertEquals("所有 startActivity 策略都必须被设备侧测试覆盖", FwStartStrategy.entries.toSet(), coveredStrategies) + } +} diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..51c6a71 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/debug/java/com/google/services/TestStartActivity.kt b/app/src/debug/java/com/google/services/TestStartActivity.kt new file mode 100644 index 0000000..ae52a74 --- /dev/null +++ b/app/src/debug/java/com/google/services/TestStartActivity.kt @@ -0,0 +1,42 @@ +/** + * ============================================================================ + * TestStartActivity.kt - Debug 专用 startActivity 测试 Activity + * ============================================================================ + * + * 功能简介: + * 提供一个无动画、无权限请求、无后台任务的极简 Activity,专门用于 + * instrumentation 测试真实触发 Native startActivity 策略。 + * + * 函数简介: + * - onCreate:创建一个简单 View,确保主线程可以快速进入 idle 状态。 + * + * @author Pangu-Immortal + * @github https://github.com/Pangu-Immortal/KeepLiveService + * @since 2.0.1 + */ +package com.google.services + +import android.app.Activity +import android.graphics.Color +import android.os.Bundle +import android.view.View + +/** + * Debug 专用 startActivity 测试 Activity。 + */ +class TestStartActivity : Activity() { + + /** + * 创建无动画测试界面。 + */ + override fun onCreate(savedInstanceState: Bundle?) { + // 先执行 Activity 标准初始化。 + super.onCreate(savedInstanceState) + // 创建一个简单 View,避免 Compose 动画导致 instrumentation 等不到 idle。 + val contentView = View(this) + // 使用纯色背景,保证绘制成本最低。 + contentView.setBackgroundColor(Color.BLACK) + // 设置测试内容视图。 + setContentView(contentView) + } +} diff --git a/framework/build.gradle.kts b/framework/build.gradle.kts index 1e445b5..bf3bfd4 100644 --- a/framework/build.gradle.kts +++ b/framework/build.gradle.kts @@ -1,3 +1,9 @@ +import org.gradle.api.GradleException +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.Delete +import org.gradle.api.tasks.Exec +import java.util.Properties + /** * =============================================================================== * Fw Android Keep-Alive Framework - Library Module Build Configuration @@ -36,10 +42,39 @@ plugins { signing } +data class FwRustAndroidTarget( + val abi: String, + val rustTriple: String, + val linkerPrefix: String +) + +val fwRustAndroidTargets = listOf( + FwRustAndroidTarget("armeabi-v7a", "armv7-linux-androideabi", "armv7a-linux-androideabi"), + FwRustAndroidTarget("arm64-v8a", "aarch64-linux-android", "aarch64-linux-android"), + FwRustAndroidTarget("x86", "i686-linux-android", "i686-linux-android"), + FwRustAndroidTarget("x86_64", "x86_64-linux-android", "x86_64-linux-android") +) +val fwAndroidNdkVersion = "27.2.12479018" +val fwBuildRust = providers.gradleProperty("fwBuildRust") + .map { value -> value.equals("true", ignoreCase = true) } + .orElse(false) +val fwRustManifest = layout.projectDirectory.file("src/main/rust/fw_rust/Cargo.toml") +val fwRustTargetDir = layout.buildDirectory.dir("rust/target") +val fwRustJniDir = layout.buildDirectory.dir("generated/rustJniLibs") +val fwRustMinSdk = 24 + +fun String.toFwTaskSuffix(): String = + split("-", "_").joinToString("") { part -> + part.replaceFirstChar { char -> char.uppercase() } + } + +fun rustLinkerEnvName(rustTriple: String): String = + "CARGO_TARGET_${rustTriple.uppercase().replace("-", "_")}_LINKER" + android { namespace = "com.service.framework" compileSdk = 36 - ndkVersion = "27.2.12479018" + ndkVersion = fwAndroidNdkVersion defaultConfig { minSdk = 24 @@ -128,6 +163,17 @@ android { } } +androidComponents { + onVariants(selector().all()) { variant -> + // 只有显式开启 Rust 构建时才把生成目录加入 jniLibs,避免默认构建误打包旧 so。 + if (fwBuildRust.get()) { + variant.sources.jniLibs?.addStaticSourceDirectory( + fwRustJniDir.get().asFile.absolutePath + ) + } + } +} + dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.media) @@ -143,6 +189,155 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) } +// ==================== Rust Native 构建配置 ==================== + +fun resolveAndroidSdkDir(): File { + val localPropertiesFile = rootProject.file("local.properties") + if (localPropertiesFile.isFile) { + val properties = Properties() + localPropertiesFile.inputStream().use { input -> properties.load(input) } + val sdkDir = properties.getProperty("sdk.dir") + if (!sdkDir.isNullOrBlank()) { + return File(sdkDir) + } + } + val envSdkDir = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT") + if (!envSdkDir.isNullOrBlank()) { + return File(envSdkDir) + } + throw GradleException("未找到 Android SDK 目录,请配置 local.properties 的 sdk.dir 或 ANDROID_HOME") +} + +fun resolveExecutable(command: String): File? { + val commandFile = File(command) + if (commandFile.isAbsolute || command.contains(File.separator)) { + return commandFile.takeIf { file -> file.canExecute() } + } + return System.getenv("PATH") + .orEmpty() + .split(File.pathSeparator) + .asSequence() + .filter { path -> path.isNotBlank() } + .map { path -> File(path, command) } + .firstOrNull { file -> file.canExecute() } +} + +fun resolveNdkToolchainBin(): File { + val ndkDir = File(resolveAndroidSdkDir(), "ndk/$fwAndroidNdkVersion") + val prebuiltDir = File(ndkDir, "toolchains/llvm/prebuilt") + val hostCandidates = listOf("darwin-x86_64", "darwin-aarch64", "linux-x86_64", "windows-x86_64") + val hostDir = hostCandidates + .map { hostName -> File(prebuiltDir, hostName) } + .firstOrNull { candidate -> candidate.isDirectory } + ?: throw GradleException("未找到 Android NDK LLVM 工具链目录:${prebuiltDir.absolutePath}") + return File(hostDir, "bin") +} + +tasks.register("cleanFwRustAndroid") { + group = "rust" + description = "清理 Fw Rust Native 产物" + delete(fwRustTargetDir, fwRustJniDir) +} + +tasks.register("checkFwRustToolchain") { + group = "rust" + description = "检查 Rust、Cargo 与 Android NDK 链接器是否可用" + doLast { + val cargoExecutable = findProperty("fwCargoPath") as String? ?: "cargo" + val cargoFile = resolveExecutable(cargoExecutable) + ?: throw GradleException("未找到 Cargo 可执行文件,请安装 Rust 或通过 -PfwCargoPath 指定 cargo 路径") + val rustcExecutable = findProperty("fwRustcPath") as String? ?: File(cargoFile.parentFile, "rustc").absolutePath + resolveExecutable(rustcExecutable) + ?: throw GradleException("未找到 rustc 可执行文件,请安装 Rust 或通过 -PfwRustcPath 指定 rustc 路径") + val toolchainBin = resolveNdkToolchainBin() + fwRustAndroidTargets.forEach { target -> + val linker = File(toolchainBin, "${target.linkerPrefix}${fwRustMinSdk}-clang") + if (!linker.canExecute()) { + throw GradleException("Rust Android 链接器不可执行:${linker.absolutePath}") + } + } + } +} + +val copyFwRustTasks = fwRustAndroidTargets.map { target -> + val suffix = target.abi.toFwTaskSuffix() + val buildTask = tasks.register("buildFwRust$suffix") { + group = "rust" + description = "构建 ${target.abi} 对应的 libfw_rust.so" + onlyIf { fwBuildRust.get() } + workingDir = fwRustManifest.asFile.parentFile + executable = findProperty("fwCargoPath") as String? ?: "cargo" + args( + "build", + "--manifest-path", + fwRustManifest.asFile.absolutePath, + "--target", + target.rustTriple, + "--release" + ) + doFirst { + if (!fwRustManifest.asFile.isFile) { + throw GradleException("Rust manifest 不存在:${fwRustManifest.asFile.absolutePath}") + } + val configuredCargo = findProperty("fwCargoPath") as String? ?: "cargo" + val cargoFile = resolveExecutable(configuredCargo) + ?: throw GradleException("未找到 Cargo 可执行文件,请安装 Rust 或通过 -PfwCargoPath 指定 cargo 路径") + val rustcExecutable = findProperty("fwRustcPath") as String? ?: File(cargoFile.parentFile, "rustc").absolutePath + val rustcFile = resolveExecutable(rustcExecutable) + ?: throw GradleException("未找到 rustc 可执行文件,请安装 Rust 或通过 -PfwRustcPath 指定 rustc 路径") + val toolchainBin = resolveNdkToolchainBin() + val linker = File(toolchainBin, "${target.linkerPrefix}${fwRustMinSdk}-clang") + if (!linker.canExecute()) { + throw GradleException("Rust Android 链接器不可执行:${linker.absolutePath}") + } + val baseRustFlags = System.getenv("RUSTFLAGS").orEmpty() + val androidRustFlags = "-C link-arg=-Wl,-z,max-page-size=16384 -C link-arg=-Wl,--gc-sections" + val rustPath = listOf(cargoFile.parentFile.absolutePath, System.getenv("PATH").orEmpty()) + .filter { value -> value.isNotBlank() } + .joinToString(File.pathSeparator) + environment("CARGO_TARGET_DIR", fwRustTargetDir.get().asFile.absolutePath) + environment(rustLinkerEnvName(target.rustTriple), linker.absolutePath) + environment("PATH", rustPath) + environment("RUSTC", rustcFile.absolutePath) + environment( + "RUSTFLAGS", + listOf(baseRustFlags, androidRustFlags).filter { value -> value.isNotBlank() }.joinToString(" ") + ) + logger.lifecycle("Fw Rust: 构建 ${target.abi},target=${target.rustTriple}") + } + } + + tasks.register("copyFwRust$suffix") { + group = "rust" + description = "复制 ${target.abi} 的 libfw_rust.so 到 Android jniLibs 产物目录" + onlyIf { fwBuildRust.get() } + dependsOn(buildTask) + from(fwRustTargetDir.map { targetDir -> + targetDir.file("${target.rustTriple}/release/libfw_rust.so") + }) + into(fwRustJniDir.map { jniDir -> jniDir.dir(target.abi) }) + rename { "libfw_rust.so" } + } +} + +val buildFwRustAndroid = tasks.register("buildFwRustAndroid") { + group = "rust" + description = "构建并打包 4 个 Android ABI 的 Rust Native 骨架库" + onlyIf { fwBuildRust.get() } + dependsOn(copyFwRustTasks) + doFirst { + logger.lifecycle("Fw Rust: 已启用 -PfwBuildRust=true,将把 libfw_rust.so 纳入 AAR") + } +} + +tasks.matching { task -> + task.name.startsWith("merge") && task.name.endsWith("JniLibFolders") +}.configureEach { + if (fwBuildRust.get()) { + dependsOn(buildFwRustAndroid) + } +} + // ==================== Maven Central 发布配置 ==================== // 从 gradle.properties 或 ~/.gradle/gradle.properties 读取发布参数 diff --git a/framework/src/androidTest/java/com/service/framework/rust/FwRustNativeInstrumentedTest.kt b/framework/src/androidTest/java/com/service/framework/rust/FwRustNativeInstrumentedTest.kt new file mode 100644 index 0000000..fdfcbe3 --- /dev/null +++ b/framework/src/androidTest/java/com/service/framework/rust/FwRustNativeInstrumentedTest.kt @@ -0,0 +1,212 @@ +/** + * ============================================================================ + * FwRustNativeInstrumentedTest.kt - Rust Native 设备侧真实触发测试 + * ============================================================================ + * + * 功能简介: + * 在 Android 设备或模拟器上真实加载 Native so,验证 Rust 进程探测、 + * Rust MediaRoute 状态机,以及公共 MediaRoute Native 回退入口。 + * + * 函数简介: + * - rustProcessSnapshot_whenRustSoPackaged_returnsRealProcData:验证 Rust 读取 /proc。 + * - rustMediaRouteNative_stateMachine_transitions:验证 Rust MediaRoute 状态迁移。 + * - mediaRoutePublicNative_backendTransitions_withoutCrash:验证公共入口可真实触发。 + * + * @author Pangu-Immortal + * @github https://github.com/Pangu-Immortal/KeepLiveService + * @since 2.0.1 + */ +package com.service.framework.rust + +import android.app.Application +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.service.framework.Fw +import com.service.framework.mediaroute.FwMediaRouteNative +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Rust Native 设备侧真实触发测试。 + */ +@RunWith(AndroidJUnit4::class) +class FwRustNativeInstrumentedTest { + + /** + * 每个用例前重置 Rust MediaRoute 服务位,避免状态串扰。 + */ + @Before + fun resetMediaRouteState() { + // 初始化最小化 Fw 配置,保证 FwLog 在测试进程中可读取 logTag。 + ensureFwConfigReady() + // Rust so 未打包时跳过直接 Rust 状态重置,公共入口测试会覆盖 C++ 回退。 + if (!FwRustNative.init(logUnavailable = false)) { + return + } + // 初始化 Rust MediaRoute 动态注册方法。 + FwRustMediaRouteNative.init() + // 清空 MediaRouteProviderService 运行位。 + FwRustMediaRouteNative.onServiceStopped() + // 清空 MediaRoute2ProviderService 运行位。 + FwRustMediaRouteNative.onService2Stopped() + } + + /** + * 每个用例后释放 WakeLock,避免影响后续测试和模拟器状态。 + */ + @After + fun releaseWakeLock() { + // 公共入口可能短暂获取 WakeLock,测试结束必须释放。 + FwMediaRouteNative.releaseWakeLock() + } + + /** + * 初始化测试专用最小配置,只打开日志,不启动任何保活策略。 + */ + private fun ensureFwConfigReady() { + // 已初始化时不重复触发框架入口。 + if (Fw.isInitialized()) { + return + } + // 获取测试进程的 Application。 + val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application + // 使用全关闭配置,避免 instrumentation 测试启动后台策略。 + Fw.init(app) { + enableForegroundService = false + enableMediaSession = false + enableOnePixelActivity = false + enableJobScheduler = false + enableWorkManager = false + enableAlarmManager = false + enableAccountSync = false + enableSystemBroadcast = false + enableBluetoothBroadcast = false + enableMediaButtonReceiver = false + enableUsbBroadcast = false + enableNfcBroadcast = false + enableMediaMountBroadcast = false + enableMediaContentObserver = false + enableContactsContentObserver = false + enableSmsContentObserver = false + enableSettingsContentObserver = false + enableFileObserver = false + enableDualProcess = false + enableNativeDaemon = false + enableNativeSocket = false + enableLockScreenActivity = false + enableFloatWindow = false + enableForceStopResistance = false + enableMediaRouteProvider = false + enableMediaRoute2Provider = false + enableMediaIntentActivity = false + enableMediaBrowserService = false + enableSilentAudio = false + enableVpnService = false + enableCompanionDevice = false + enableCallStyleNotification = false + enableMediaSessionNotification = false + enableDeviceAdmin = false + enableTileService = false + enableWidget = false + enableDreamService = false + enableDebugLog = true + logTag = "FwTest" + } + } + + /** + * 验证 Rust so 打包时可以真实读取 /proc 进程信息。 + */ + @Test + fun rustProcessSnapshot_whenRustSoPackaged_returnsRealProcData() { + // 默认 C++ 构建不包含 Rust so,此用例只在 -PfwBuildRust=true 矩阵执行。 + assumeTrue("Rust so 未打包,跳过 Rust 专属进程探测测试", FwRustNative.init(logUnavailable = false)) + + // 真实触发 Rust JNI 读取当前进程快照。 + val snapshot = FwRustNative.processSnapshotOrNull() + + // Rust 进程快照必须返回有效对象。 + assertNotNull("Rust 进程快照不应为空", snapshot) + // Rust Native 当前版本号固定为 1。 + assertEquals("Rust Native 版本号应匹配", 1, snapshot!!.versionCode) + // 真实系统总内存必须大于 0。 + assertTrue("系统总内存必须大于 0", snapshot.memoryTotalKb > 0L) + // 真实系统可用内存必须大于 0。 + assertTrue("系统可用内存必须大于 0", snapshot.memoryAvailableKb > 0L) + // /proc/self/status 必须包含 Pid 字段。 + assertTrue("进程状态必须包含 Pid", snapshot.processStatus.contains("Pid:")) + // /proc 进程数量必须大于 0。 + assertTrue("系统进程数量必须大于 0", snapshot.processCount > 0) + } + + /** + * 验证 Rust MediaRoute 状态机能真实完成 2 -> 1 -> 0 -> 1 -> 2 状态迁移。 + */ + @Test + fun rustMediaRouteNative_stateMachine_transitions() { + // 默认 C++ 构建不包含 Rust so,此用例只在 -PfwBuildRust=true 矩阵执行。 + assumeTrue("Rust so 未打包,跳过 Rust MediaRoute 状态机测试", FwRustNative.init(logUnavailable = false)) + + // 初始化 Rust MediaRoute 入口。 + assertTrue("Rust MediaRoute 应初始化成功", FwRustMediaRouteNative.init()) + // 两个服务都停止时应为异常状态。 + assertEquals("初始停止状态应为异常", 2, FwRustMediaRouteNative.getServiceStatus()) + + // 真实触发 MediaRouteProviderService 启动状态。 + FwRustMediaRouteNative.onServiceStarted("com.google.services", "FwMediaRouteProviderService") + // 单服务运行时应为警告状态。 + assertEquals("单服务运行应为警告", 1, FwRustMediaRouteNative.getServiceStatus()) + // 单服务运行时心跳应返回成功。 + assertTrue("单服务运行时心跳应成功", FwRustMediaRouteNative.performHeartbeat()) + + // 真实触发 MediaRoute2ProviderService 启动状态。 + FwRustMediaRouteNative.onService2Started("com.google.services", "FwMediaRoute2ProviderService") + // 双服务运行时应为正常状态。 + assertEquals("双服务运行应为正常", 0, FwRustMediaRouteNative.getServiceStatus()) + // WakeLock 检查计数应能真实调用且不抛异常。 + FwRustMediaRouteNative.checkWakeLock() + + // 停止第一个服务后应降级为警告。 + FwRustMediaRouteNative.onServiceStopped() + assertEquals("停止一个服务后应为警告", 1, FwRustMediaRouteNative.getServiceStatus()) + // 停止第二个服务后应降级为异常。 + FwRustMediaRouteNative.onService2Stopped() + assertEquals("停止两个服务后应为异常", 2, FwRustMediaRouteNative.getServiceStatus()) + } + + /** + * 验证公共 MediaRoute Native 入口在当前打包矩阵下可真实触发并完成状态迁移。 + */ + @Test + fun mediaRoutePublicNative_backendTransitions_withoutCrash() { + // 获取被测应用上下文。 + val context = InstrumentationRegistry.getInstrumentation().targetContext + + // 初始化公共 Native 入口;Rust 存在时走 Rust,不存在时走 C++ 回退。 + assertTrue("公共 MediaRoute Native 入口应可初始化", FwMediaRouteNative.init(context)) + // 清空两个服务状态。 + FwMediaRouteNative.onServiceStopped() + FwMediaRouteNative.onService2Stopped() + // 两个服务都停止时应为异常状态。 + assertEquals("公共入口停止态应为异常", 2, FwMediaRouteNative.getServiceStatus()) + + // 真实触发第一个服务启动状态。 + FwMediaRouteNative.onServiceStarted(context.packageName, "FwMediaRouteProviderService") + // 单服务运行时应为警告状态。 + assertEquals("公共入口单服务运行应为警告", 1, FwMediaRouteNative.getServiceStatus()) + // 真实触发第二个服务启动状态。 + FwMediaRouteNative.onService2Started(context.packageName, "FwMediaRoute2ProviderService") + // 双服务运行时应为正常状态。 + assertEquals("公共入口双服务运行应为正常", 0, FwMediaRouteNative.getServiceStatus()) + // 真实触发心跳。 + assertTrue("公共入口双服务运行时心跳应成功", FwMediaRouteNative.performHeartbeat()) + // 真实触发 WakeLock 检查,验证不会产生 JNI 或 Runtime 崩溃。 + FwMediaRouteNative.checkWakeLock() + } +} diff --git a/framework/src/main/java/com/service/framework/health/FwHealthMonitor.kt b/framework/src/main/java/com/service/framework/health/FwHealthMonitor.kt index f59a58d..041ec1e 100644 --- a/framework/src/main/java/com/service/framework/health/FwHealthMonitor.kt +++ b/framework/src/main/java/com/service/framework/health/FwHealthMonitor.kt @@ -29,6 +29,7 @@ import com.service.framework.mediaroute.FwMediaRouteManager import com.service.framework.native.FwNative import com.service.framework.observer.ContentObserverManager import com.service.framework.observer.FileObserverManager +import com.service.framework.rust.FwRustNative import com.service.framework.service.FwForegroundService import com.service.framework.strategy.AlarmStrategy import com.service.framework.strategy.BatteryOptimizationManager @@ -281,6 +282,8 @@ object FwHealthMonitor { autoRecover: Boolean ): List { val nativeAvailable = FwNative.isAvailable() || FwNative.init(context) + val rustSnapshot = FwRustNative.processSnapshotOrNull() + val rustMessage = rustSnapshot?.toHealthMessage() val daemonHealth = if (!config.enableNativeDaemon) { disabled(FwStrategyKey.NATIVE_DAEMON, "配置关闭") } else if (!nativeAvailable) { @@ -299,7 +302,10 @@ object FwHealthMonitor { state = if (running || recovered) FwStrategyHealthState.RUNNING else FwStrategyHealthState.STOPPED, enabled = true, supported = true, - message = if (running) "Native 守护运行中" else "Native 守护未运行", + message = listOfNotNull( + if (running) "Native 守护运行中" else "Native 守护未运行", + rustMessage + ).joinToString(";"), recovered = recovered ) } diff --git a/framework/src/main/java/com/service/framework/mediaroute/FwMediaRouteNative.kt b/framework/src/main/java/com/service/framework/mediaroute/FwMediaRouteNative.kt index bebb141..192f063 100644 --- a/framework/src/main/java/com/service/framework/mediaroute/FwMediaRouteNative.kt +++ b/framework/src/main/java/com/service/framework/mediaroute/FwMediaRouteNative.kt @@ -27,6 +27,7 @@ package com.service.framework.mediaroute import android.content.Context import android.os.PowerManager +import com.service.framework.rust.FwRustMediaRouteNative import com.service.framework.util.FwLog /** @@ -41,8 +42,11 @@ object FwMediaRouteNative { private const val TAG = "FwMediaRouteNative" - // Native 库加载状态 - private var isLoaded = false + // C++ Native 库加载状态 + private var isCppLoaded = false + + // Rust Native 库加载状态 + private var isRustLoaded = false // WakeLock 实例 private var wakeLock: PowerManager.WakeLock? = null @@ -59,22 +63,28 @@ object FwMediaRouteNative { fun init(context: Context): Boolean { appContext = context.applicationContext - if (isLoaded) { + if (isRustLoaded || isCppLoaded) { FwLog.d("$TAG: 已初始化,跳过") return true } + isRustLoaded = FwRustMediaRouteNative.init() + if (isRustLoaded) { + FwLog.d("$TAG: Rust MediaRoute Native 库加载成功") + return true + } + return try { System.loadLibrary("fw_mediaroute") - isLoaded = true - FwLog.d("$TAG: Native 库加载成功") + isCppLoaded = true + FwLog.d("$TAG: C++ Native 库加载成功") // 初始化 Native 层 nativeInit() true } catch (e: UnsatisfiedLinkError) { - FwLog.e("$TAG: Native 库加载失败 - ${e.message}", e) - isLoaded = false + FwLog.e("$TAG: Native 库加载失败,Rust 与 C++ 均不可用 - ${e.message}", e) + isCppLoaded = false false } } @@ -82,7 +92,7 @@ object FwMediaRouteNative { /** * 检查 Native 模块是否可用 */ - fun isAvailable(): Boolean = isLoaded + fun isAvailable(): Boolean = isRustLoaded || isCppLoaded // ==================== WakeLock 管理 ==================== @@ -159,7 +169,9 @@ object FwMediaRouteNative { // 短暂获取 WakeLock(1秒) if (acquireWakeLock(timeout = 1000L)) { // Native 层检查 - if (isLoaded) { + if (isRustLoaded) { + FwRustMediaRouteNative.checkWakeLock() + } else if (isCppLoaded) { nativeCheckWakeLock() } } @@ -182,7 +194,9 @@ object FwMediaRouteNative { */ fun onServiceStarted(packageName: String, serviceName: String) { FwLog.d("$TAG: 服务已启动 - $serviceName") - if (isLoaded) { + if (isRustLoaded) { + FwRustMediaRouteNative.onServiceStarted(packageName, serviceName) + } else if (isCppLoaded) { nativeOnServiceStarted(packageName, serviceName) } } @@ -192,7 +206,9 @@ object FwMediaRouteNative { */ fun onServiceStopped() { FwLog.d("$TAG: 服务已停止") - if (isLoaded) { + if (isRustLoaded) { + FwRustMediaRouteNative.onServiceStopped() + } else if (isCppLoaded) { nativeOnServiceStopped() } } @@ -205,7 +221,9 @@ object FwMediaRouteNative { */ fun onService2Started(packageName: String, serviceName: String) { FwLog.d("$TAG: MediaRoute2 服务已启动 - $serviceName") - if (isLoaded) { + if (isRustLoaded) { + FwRustMediaRouteNative.onService2Started(packageName, serviceName) + } else if (isCppLoaded) { nativeOnService2Started(packageName, serviceName) } } @@ -215,7 +233,9 @@ object FwMediaRouteNative { */ fun onService2Stopped() { FwLog.d("$TAG: MediaRoute2 服务已停止") - if (isLoaded) { + if (isRustLoaded) { + FwRustMediaRouteNative.onService2Stopped() + } else if (isCppLoaded) { nativeOnService2Stopped() } } @@ -230,11 +250,13 @@ object FwMediaRouteNative { * @return 心跳是否成功 */ fun performHeartbeat(): Boolean { - return if (isLoaded) { - nativePerformHeartbeat() - } else { - FwLog.w("$TAG: Native 模块未加载,跳过心跳") - false + return when { + isRustLoaded -> FwRustMediaRouteNative.performHeartbeat() + isCppLoaded -> nativePerformHeartbeat() + else -> { + FwLog.w("$TAG: Native 模块未加载,跳过心跳") + false + } } } @@ -244,10 +266,10 @@ object FwMediaRouteNative { * @return 状态码:0=正常, 1=警告, 2=异常 */ fun getServiceStatus(): Int { - return if (isLoaded) { - nativeGetServiceStatus() - } else { - 2 // 未加载视为异常 + return when { + isRustLoaded -> FwRustMediaRouteNative.getServiceStatus() + isCppLoaded -> nativeGetServiceStatus() + else -> 2 // 未加载视为异常 } } diff --git a/framework/src/main/java/com/service/framework/native/FwNative.kt b/framework/src/main/java/com/service/framework/native/FwNative.kt index 216c546..f5a9f90 100644 --- a/framework/src/main/java/com/service/framework/native/FwNative.kt +++ b/framework/src/main/java/com/service/framework/native/FwNative.kt @@ -25,6 +25,7 @@ package com.service.framework.native import android.content.Context import android.content.Intent import android.os.Build +import com.service.framework.rust.FwRustNative import com.service.framework.util.FwLog /** @@ -312,6 +313,13 @@ object FwNative { * 获取可读的内存信息 */ fun getMemoryInfoReadable(): String { + FwRustNative.processSnapshotOrNull()?.let { snapshot -> + val totalMB = snapshot.memoryTotalKb / 1024 + val freeMB = snapshot.memoryFreeKb / 1024 + val availableMB = snapshot.memoryAvailableKb / 1024 + return "总内存: ${totalMB}MB, 空闲: ${freeMB}MB, 可用: ${availableMB}MB (Rust)" + } + if (!isAvailable()) return "Native 模块不可用" return try { @@ -329,6 +337,16 @@ object FwNative { * 获取当前进程信息摘要 */ fun getProcessSummary(): String { + FwRustNative.processSnapshotOrNull()?.let { snapshot -> + return buildString { + appendLine("OOM adj: ${snapshot.oomAdj}") + appendLine("进程优先级: Rust 未迁移") + appendLine("Root 权限: ${snapshot.hasRoot}") + appendLine("系统进程数: ${snapshot.processCount}") + append(snapshot.processStatus) + } + } + if (!isAvailable()) return "Native 模块不可用" return try { diff --git a/framework/src/main/java/com/service/framework/rust/FwRustMediaRouteNative.kt b/framework/src/main/java/com/service/framework/rust/FwRustMediaRouteNative.kt new file mode 100644 index 0000000..ee02667 --- /dev/null +++ b/framework/src/main/java/com/service/framework/rust/FwRustMediaRouteNative.kt @@ -0,0 +1,153 @@ +/** + * ============================================================================ + * FwRustMediaRouteNative.kt - Rust MediaRoute Native 桥接入口 + * ============================================================================ + * + * 功能简介: + * 封装 Rust 版 MediaRoute 服务状态与心跳逻辑。当前只迁移状态记录、 + * WakeLock 检查计数和心跳判断,真实 WakeLock 获取仍由 Kotlin 层完成。 + * + * 函数简介: + * - init:初始化 Rust MediaRoute 状态。 + * - checkWakeLock:记录 WakeLock 检查心跳。 + * - onServiceStarted / onServiceStopped:记录 MediaRouteProviderService 状态。 + * - onService2Started / onService2Stopped:记录 MediaRoute2ProviderService 状态。 + * - performHeartbeat:执行 Rust 心跳判断。 + * - getServiceStatus:读取服务状态码。 + * + * @author Pangu-Immortal + * @github https://github.com/Pangu-Immortal/KeepLiveService + * @since 2.0.1 + */ +package com.service.framework.rust + +import com.service.framework.util.FwLog + +/** + * Rust MediaRoute Native 桥接入口。 + */ +internal object FwRustMediaRouteNative { + + private const val TAG = "FwRustMediaRouteNative" + + // Rust MediaRoute 状态是否已经初始化。 + private var isInitialized = false + + /** + * 初始化 Rust MediaRoute 状态。 + */ + fun init(): Boolean { + if (isInitialized) { + FwLog.d("$TAG: Rust MediaRoute 已初始化,跳过") + return true + } + if (!FwRustNative.isAvailable()) { + FwLog.d("$TAG: Rust Native 不可用,MediaRoute 使用 C++ 回退") + return false + } + return try { + nativeInit() + isInitialized = true + FwLog.d("$TAG: Rust MediaRoute 初始化成功") + true + } catch (e: UnsatisfiedLinkError) { + FwLog.e("$TAG: Rust MediaRoute 符号不可用 - ${e.message}", e) + false + } catch (e: RuntimeException) { + FwLog.e("$TAG: Rust MediaRoute 初始化异常 - ${e.message}", e) + false + } + } + + /** + * 检查 Rust MediaRoute 是否可用。 + */ + fun isAvailable(): Boolean = isInitialized || init() + + /** + * 记录 WakeLock 检查。 + */ + fun checkWakeLock() { + if (isAvailable()) { + nativeCheckWakeLock() + } + } + + /** + * 记录 MediaRouteProviderService 启动。 + */ + fun onServiceStarted(packageName: String, serviceName: String) { + if (isAvailable()) { + nativeOnServiceStarted(packageName, serviceName) + } + } + + /** + * 记录 MediaRouteProviderService 停止。 + */ + fun onServiceStopped() { + if (isAvailable()) { + nativeOnServiceStopped() + } + } + + /** + * 记录 MediaRoute2ProviderService 启动。 + */ + fun onService2Started(packageName: String, serviceName: String) { + if (isAvailable()) { + nativeOnService2Started(packageName, serviceName) + } + } + + /** + * 记录 MediaRoute2ProviderService 停止。 + */ + fun onService2Stopped() { + if (isAvailable()) { + nativeOnService2Stopped() + } + } + + /** + * 执行 Rust MediaRoute 心跳。 + */ + fun performHeartbeat(): Boolean { + return isAvailable() && nativePerformHeartbeat() + } + + /** + * 获取 Rust MediaRoute 服务状态。 + */ + fun getServiceStatus(): Int { + return if (isAvailable()) { + nativeGetServiceStatus() + } else { + 2 + } + } + + @JvmStatic + private external fun nativeInit() + + @JvmStatic + private external fun nativeCheckWakeLock() + + @JvmStatic + private external fun nativeOnServiceStarted(packageName: String, serviceName: String) + + @JvmStatic + private external fun nativeOnServiceStopped() + + @JvmStatic + private external fun nativeOnService2Started(packageName: String, serviceName: String) + + @JvmStatic + private external fun nativeOnService2Stopped() + + @JvmStatic + private external fun nativePerformHeartbeat(): Boolean + + @JvmStatic + private external fun nativeGetServiceStatus(): Int +} diff --git a/framework/src/main/java/com/service/framework/rust/FwRustNative.kt b/framework/src/main/java/com/service/framework/rust/FwRustNative.kt new file mode 100644 index 0000000..493f62d --- /dev/null +++ b/framework/src/main/java/com/service/framework/rust/FwRustNative.kt @@ -0,0 +1,172 @@ +/** + * ============================================================================ + * FwRustNative.kt - Rust Native 迁移桥接入口 + * ============================================================================ + * + * 功能简介: + * 封装 Rust Native 骨架库的加载和能力探测,当前只用于验证 Rust so + * 构建、打包、JNI 动态注册链路,不替换现有 C++ JNI 业务逻辑。 + * + * 函数简介: + * - init:加载 libfw_rust.so,并完成 JNI 动态注册。 + * - isAvailable:检查 Rust Native 骨架是否可用。 + * - versionCode:读取 Rust Native 骨架版本号。 + * - processSnapshotOrNull:读取 Rust 侧进程状态快照。 + * + * @author Pangu-Immortal + * @github https://github.com/Pangu-Immortal/KeepLiveService + * @since 2.0.1 + */ +package com.service.framework.rust + +import com.service.framework.util.FwLog + +/** + * Rust Native 迁移桥接入口。 + * + * 当前类保持内部使用,不接入 `Fw.init()` 主链路,避免缺少 Rust so 时影响已有用户。 + */ +internal object FwRustNative { + + private const val TAG = "FwRustNative" + + // 记录 Rust so 是否已经成功加载。 + private var isLoaded = false + + // 记录是否已经尝试过加载,避免重复输出失败日志。 + private var hasTriedLoad = false + + /** + * 初始化 Rust Native 骨架库。 + * + * @return Rust so 是否加载并完成 JNI 注册 + */ + fun init(logUnavailable: Boolean = true): Boolean { + if (isLoaded) { + FwLog.d("$TAG: Rust Native 已初始化,跳过") + return true + } + if (hasTriedLoad) { + FwLog.d("$TAG: Rust Native 已尝试加载且当前不可用,跳过重复加载") + return false + } + hasTriedLoad = true + return try { + // 加载可选 Rust 骨架库;未启用 -PfwBuildRust=true 的本地构建不会包含该 so。 + System.loadLibrary("fw_rust") + // 调用动态注册方法,确保 JNI_OnLoad 已完成注册。 + val available = nativeIsAvailable() + isLoaded = available + FwLog.d("$TAG: Rust Native 加载${if (available) "成功" else "失败"}") + available + } catch (e: UnsatisfiedLinkError) { + if (logUnavailable) { + FwLog.w("$TAG: Rust Native 库不可用 - ${e.message}") + } else { + FwLog.d("$TAG: Rust Native 库未打包,跳过 Rust 进程探测") + } + isLoaded = false + false + } catch (e: RuntimeException) { + FwLog.e("$TAG: Rust Native 初始化异常 - ${e.message}", e) + isLoaded = false + false + } + } + + /** + * 检查 Rust Native 骨架是否可用。 + */ + fun isAvailable(): Boolean = isLoaded || init(logUnavailable = false) + + /** + * 获取 Rust Native 骨架版本号。 + */ + fun versionCode(): Int { + if (!isAvailable()) { + return 0 + } + return try { + nativeVersionCode() + } catch (e: UnsatisfiedLinkError) { + FwLog.e("$TAG: nativeVersionCode 符号不可用 - ${e.message}", e) + 0 + } + } + + /** + * 读取 Rust Native 进程状态快照。 + * + * 当前快照只覆盖只读系统信息,不涉及 Binder、fork、Socket 等高风险路径。 + */ + fun processSnapshotOrNull(): FwRustProcessSnapshot? { + if (!isAvailable()) { + return null + } + return try { + val memoryInfo = nativeGetMemoryInfo() + if (memoryInfo.size < 3) { + FwLog.w("$TAG: Rust Native 内存数组长度异常:${memoryInfo.size}") + return null + } + FwRustProcessSnapshot( + versionCode = nativeVersionCode(), + oomAdj = nativeGetOomAdj(), + memoryTotalKb = memoryInfo[0], + memoryFreeKb = memoryInfo[1], + memoryAvailableKb = memoryInfo[2], + processStatus = nativeGetProcessStatus(), + hasRoot = nativeCheckRoot(), + processCount = nativeGetProcessCount() + ) + } catch (e: UnsatisfiedLinkError) { + FwLog.e("$TAG: Rust Native 进程探测符号不可用 - ${e.message}", e) + null + } catch (e: RuntimeException) { + FwLog.e("$TAG: Rust Native 进程探测异常 - ${e.message}", e) + null + } + } + + @JvmStatic + private external fun nativeIsAvailable(): Boolean + + @JvmStatic + private external fun nativeVersionCode(): Int + + @JvmStatic + private external fun nativeGetOomAdj(): Int + + @JvmStatic + private external fun nativeGetMemoryInfo(): LongArray + + @JvmStatic + private external fun nativeGetProcessStatus(): String + + @JvmStatic + private external fun nativeCheckRoot(): Boolean + + @JvmStatic + private external fun nativeGetProcessCount(): Int +} + +/** + * Rust Native 进程状态快照。 + */ +internal data class FwRustProcessSnapshot( + val versionCode: Int, + val oomAdj: Int, + val memoryTotalKb: Long, + val memoryFreeKb: Long, + val memoryAvailableKb: Long, + val processStatus: String, + val hasRoot: Boolean, + val processCount: Int +) { + /** + * 转换为健康巡检日志摘要。 + */ + fun toHealthMessage(): String { + return "Rust Native v$versionCode 可用,OOM=$oomAdj,进程数=$processCount,Root=$hasRoot,内存可用=${memoryAvailableKb / 1024}MB" + } +} diff --git a/framework/src/main/rust/fw_rust/Cargo.lock b/framework/src/main/rust/fw_rust/Cargo.lock new file mode 100644 index 0000000..4af500e --- /dev/null +++ b/framework/src/main/rust/fw_rust/Cargo.lock @@ -0,0 +1,258 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "fw_rust" +version = "0.1.0" +dependencies = [ + "jni", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/framework/src/main/rust/fw_rust/Cargo.toml b/framework/src/main/rust/fw_rust/Cargo.toml new file mode 100644 index 0000000..b124d8c --- /dev/null +++ b/framework/src/main/rust/fw_rust/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "fw_rust" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "fw_rust" +crate-type = ["cdylib"] + +[dependencies] +jni = "0.21.1" + +[profile.release] +opt-level = "z" +lto = "fat" +codegen-units = 1 +panic = "abort" +strip = "symbols" diff --git a/framework/src/main/rust/fw_rust/src/lib.rs b/framework/src/main/rust/fw_rust/src/lib.rs new file mode 100644 index 0000000..28110fc --- /dev/null +++ b/framework/src/main/rust/fw_rust/src/lib.rs @@ -0,0 +1,536 @@ +//! ============================================================================ +//! fw_rust - Rust Native 骨架库 +//! ============================================================================ +//! +//! 功能简介: +//! 1. 提供 Rust JNI 动态注册入口,验证 Android Rust so 构建链路。 +//! 2. 暂不承载 C++ 业务迁移,避免影响现有 fw_native 行为。 +//! 3. 后续可逐步迁移 JNI 外层、字符串保护和高适配度 Native 模块。 +//! +//! 函数简介: +//! - JNI_OnLoad:加载 so 后注册 Kotlin 内部探测方法。 +//! - native_is_available:返回 Rust Native 骨架可用状态。 +//! - native_version_code:返回 Rust Native 骨架版本号。 +//! - native_get_oom_adj:读取当前进程 OOM adj。 +//! - native_get_memory_info:读取系统内存信息。 +//! - native_get_process_status:读取当前进程关键状态。 +//! - native_check_root:检测 root / Magisk 痕迹。 +//! - native_get_process_count:统计 /proc 中的进程数量。 +//! - MediaRoute native 系列函数:迁移 MediaRoute 服务状态与心跳逻辑。 +//! ============================================================================ + +use jni::objects::{JClass, JString}; +use jni::sys::{jboolean, jint, jlong, jlongArray, jstring, JNI_FALSE, JNI_TRUE, JNI_VERSION_1_6}; +use jni::{JNIEnv, JavaVM, NativeMethod}; +use std::ffi::c_void; +use std::fs; +use std::path::Path; +use std::ptr; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering}; +use std::sync::{Mutex, OnceLock}; +use std::time::Instant; + +const FW_RUST_VERSION_CODE: jint = 1; +const FW_RUST_NATIVE_CLASS: &str = "com/service/framework/rust/FwRustNative"; +const FW_RUST_MEDIA_ROUTE_CLASS: &str = "com/service/framework/rust/FwRustMediaRouteNative"; +const DEFAULT_OOM_SCORE_ADJ: jint = 1000; + +/// MediaRoute 服务名称状态。 +#[derive(Default)] +struct MediaRouteNames { + package_name: String, + service1_name: String, + service2_name: String, +} + +/// Rust 版 MediaRoute 服务状态。 +struct MediaRouteState { + service1_running: AtomicBool, + service2_running: AtomicBool, + initialized: AtomicBool, + boot_time: Instant, + last_heartbeat_ms: AtomicI64, + heartbeat_count: AtomicI32, + names: Mutex, +} + +impl MediaRouteState { + /// 创建默认服务状态。 + fn new() -> Self { + Self { + service1_running: AtomicBool::new(false), + service2_running: AtomicBool::new(false), + initialized: AtomicBool::new(false), + boot_time: Instant::now(), + last_heartbeat_ms: AtomicI64::new(0), + heartbeat_count: AtomicI32::new(0), + names: Mutex::new(MediaRouteNames::default()), + } + } + + /// 获取单调时间毫秒。 + fn now_ms(&self) -> i64 { + self.boot_time.elapsed().as_millis() as i64 + } + + /// 初始化状态。 + fn init(&self) { + self.service1_running.store(false, Ordering::SeqCst); + self.service2_running.store(false, Ordering::SeqCst); + self.last_heartbeat_ms + .store(self.now_ms(), Ordering::SeqCst); + self.heartbeat_count.store(0, Ordering::SeqCst); + self.initialized.store(true, Ordering::SeqCst); + if let Ok(mut names) = self.names.lock() { + *names = MediaRouteNames::default(); + } + } + + /// 记录 WakeLock 检查心跳。 + fn check_wake_lock(&self) { + self.last_heartbeat_ms + .store(self.now_ms(), Ordering::SeqCst); + self.heartbeat_count.fetch_add(1, Ordering::SeqCst); + } + + /// 记录 MediaRouteProviderService 启动。 + fn on_service_started(&self, package_name: String, service_name: String) { + self.service1_running.store(true, Ordering::SeqCst); + self.last_heartbeat_ms + .store(self.now_ms(), Ordering::SeqCst); + if let Ok(mut names) = self.names.lock() { + names.package_name = package_name; + names.service1_name = service_name; + } + } + + /// 记录 MediaRouteProviderService 停止。 + fn on_service_stopped(&self) { + self.service1_running.store(false, Ordering::SeqCst); + } + + /// 记录 MediaRoute2ProviderService 启动。 + fn on_service2_started(&self, package_name: String, service_name: String) { + self.service2_running.store(true, Ordering::SeqCst); + self.last_heartbeat_ms + .store(self.now_ms(), Ordering::SeqCst); + if let Ok(mut names) = self.names.lock() { + names.package_name = package_name; + names.service2_name = service_name; + } + } + + /// 记录 MediaRoute2ProviderService 停止。 + fn on_service2_stopped(&self) { + self.service2_running.store(false, Ordering::SeqCst); + } + + /// 执行 MediaRoute 心跳。 + fn perform_heartbeat(&self) -> bool { + self.last_heartbeat_ms + .store(self.now_ms(), Ordering::SeqCst); + self.heartbeat_count.fetch_add(1, Ordering::SeqCst); + self.service1_running.load(Ordering::SeqCst) || self.service2_running.load(Ordering::SeqCst) + } + + /// 获取服务状态码:0=正常,1=警告,2=异常。 + fn service_status(&self) -> jint { + let service1_ok = self.service1_running.load(Ordering::SeqCst); + let service2_ok = self.service2_running.load(Ordering::SeqCst); + match (service1_ok, service2_ok) { + (true, true) => 0, + (true, false) | (false, true) => 1, + (false, false) => 2, + } + } +} + +/// 获取全局 MediaRoute 状态。 +fn media_route_state() -> &'static MediaRouteState { + static STATE: OnceLock = OnceLock::new(); + STATE.get_or_init(MediaRouteState::new) +} + +/// 批量注册 JNI 方法。 +fn register_methods( + env: &mut JNIEnv<'_>, + class_name: &str, + methods: &[NativeMethod], +) -> jni::errors::Result<()> { + // 查找 Kotlin 内部桥接类。 + let target_class = env.find_class(class_name)?; + // 统一使用动态注册隐藏具体业务函数名。 + env.register_native_methods(target_class, methods)?; + Ok(()) +} + +/// 注册 Rust 通用进程探测方法。 +fn register_process_native_methods(env: &mut JNIEnv<'_>) -> jni::errors::Result<()> { + let methods = [ + NativeMethod { + name: "nativeIsAvailable".into(), + sig: "()Z".into(), + fn_ptr: native_is_available as *mut c_void, + }, + NativeMethod { + name: "nativeVersionCode".into(), + sig: "()I".into(), + fn_ptr: native_version_code as *mut c_void, + }, + NativeMethod { + name: "nativeGetOomAdj".into(), + sig: "()I".into(), + fn_ptr: native_get_oom_adj as *mut c_void, + }, + NativeMethod { + name: "nativeGetMemoryInfo".into(), + sig: "()[J".into(), + fn_ptr: native_get_memory_info as *mut c_void, + }, + NativeMethod { + name: "nativeGetProcessStatus".into(), + sig: "()Ljava/lang/String;".into(), + fn_ptr: native_get_process_status as *mut c_void, + }, + NativeMethod { + name: "nativeCheckRoot".into(), + sig: "()Z".into(), + fn_ptr: native_check_root as *mut c_void, + }, + NativeMethod { + name: "nativeGetProcessCount".into(), + sig: "()I".into(), + fn_ptr: native_get_process_count as *mut c_void, + }, + ]; + register_methods(env, FW_RUST_NATIVE_CLASS, &methods) +} + +/// 注册 Rust MediaRoute 方法。 +fn register_media_route_native_methods(env: &mut JNIEnv<'_>) -> jni::errors::Result<()> { + let methods = [ + NativeMethod { + name: "nativeInit".into(), + sig: "()V".into(), + fn_ptr: native_media_route_init as *mut c_void, + }, + NativeMethod { + name: "nativeCheckWakeLock".into(), + sig: "()V".into(), + fn_ptr: native_media_route_check_wake_lock as *mut c_void, + }, + NativeMethod { + name: "nativeOnServiceStarted".into(), + sig: "(Ljava/lang/String;Ljava/lang/String;)V".into(), + fn_ptr: native_media_route_on_service_started as *mut c_void, + }, + NativeMethod { + name: "nativeOnServiceStopped".into(), + sig: "()V".into(), + fn_ptr: native_media_route_on_service_stopped as *mut c_void, + }, + NativeMethod { + name: "nativeOnService2Started".into(), + sig: "(Ljava/lang/String;Ljava/lang/String;)V".into(), + fn_ptr: native_media_route_on_service2_started as *mut c_void, + }, + NativeMethod { + name: "nativeOnService2Stopped".into(), + sig: "()V".into(), + fn_ptr: native_media_route_on_service2_stopped as *mut c_void, + }, + NativeMethod { + name: "nativePerformHeartbeat".into(), + sig: "()Z".into(), + fn_ptr: native_media_route_perform_heartbeat as *mut c_void, + }, + NativeMethod { + name: "nativeGetServiceStatus".into(), + sig: "()I".into(), + fn_ptr: native_media_route_get_service_status as *mut c_void, + }, + ]; + register_methods(env, FW_RUST_MEDIA_ROUTE_CLASS, &methods) +} + +/// 注册 Kotlin 内部桥接类使用的 Native 方法。 +fn register_native_methods(vm: &JavaVM) -> jni::errors::Result<()> { + // 通过当前加载线程获取 JNI 环境,保证注册动作发生在系统加载 so 的生命周期内。 + let mut env = vm.get_env()?; + // 分组注册,便于后续继续迁移其他 Native 模块。 + register_process_native_methods(&mut env)?; + register_media_route_native_methods(&mut env)?; + Ok(()) +} + +/// 读取 UTF-8 文本文件,失败时返回空字符串。 +fn read_text(path: &str) -> String { + // /proc 文件通常很小,直接读取可以减少 JNI 外层复杂度。 + fs::read_to_string(path).unwrap_or_default() +} + +/// 读取单行整数,失败时返回默认值。 +fn read_first_i32(path: &str, default_value: jint) -> jint { + // 只解析第一行,避免 /proc 文件带来的额外内容干扰。 + read_text(path) + .lines() + .next() + .and_then(|line| line.trim().parse::().ok()) + .unwrap_or(default_value) +} + +/// 读取当前进程 OOM adj,兼容新旧 /proc 路径。 +fn read_oom_adj() -> jint { + // Android 新版本优先使用 oom_score_adj。 + if Path::new("/proc/self/oom_score_adj").exists() { + return read_first_i32("/proc/self/oom_score_adj", DEFAULT_OOM_SCORE_ADJ); + } + // 旧版本 oom_adj 取值范围较小,转换成近似 oom_score_adj。 + if Path::new("/proc/self/oom_adj").exists() { + let old_adj = read_first_i32("/proc/self/oom_adj", 15); + return old_adj * 1000 / 17; + } + DEFAULT_OOM_SCORE_ADJ +} + +/// 从 /proc/meminfo 中解析指定字段。 +fn parse_meminfo_value(line: &str, key: &str) -> Option { + // 每行格式通常是 `MemTotal: 123456 kB`。 + line.strip_prefix(key)? + .split_whitespace() + .next() + .and_then(|value| value.parse::().ok()) +} + +/// 读取系统内存信息,单位保持 KB,与现有 C++ 返回值一致。 +fn read_memory_info() -> [jlong; 3] { + let mut total_kb: jlong = 0; + let mut free_kb: jlong = 0; + let mut available_kb: jlong = 0; + for line in read_text("/proc/meminfo").lines() { + if let Some(value) = parse_meminfo_value(line, "MemTotal:") { + total_kb = value; + } else if let Some(value) = parse_meminfo_value(line, "MemFree:") { + free_kb = value; + } else if let Some(value) = parse_meminfo_value(line, "MemAvailable:") { + available_kb = value; + } + } + [total_kb, free_kb, available_kb] +} + +/// 判断 /proc/self/status 的行是否需要返回给 Kotlin。 +fn should_keep_status_line(line: &str) -> bool { + // 与现有 C++ 逻辑保持一致,只返回调用方实际会展示的关键字段。 + line.starts_with("Name:") + || line.starts_with("State:") + || line.starts_with("Pid:") + || line.starts_with("PPid:") + || line.starts_with("Threads:") + || line.starts_with("VmSize:") + || line.starts_with("VmRSS:") + || line.starts_with("VmPeak:") +} + +/// 读取当前进程关键状态。 +fn read_process_status() -> String { + let status = read_text("/proc/self/status"); + if status.is_empty() { + return "无法读取进程状态".to_string(); + } + let mut result = String::new(); + for line in status.lines().filter(|line| should_keep_status_line(line)) { + result.push_str(line); + result.push('\n'); + } + result +} + +/// 从 /proc/self/status 中读取真实 UID。 +fn read_real_uid() -> Option { + // Uid 行格式通常为 `Uid:\t10000\t10000\t10000\t10000`。 + read_text("/proc/self/status") + .lines() + .find_map(|line| line.strip_prefix("Uid:")) + .and_then(|rest| rest.split_whitespace().next()) + .and_then(|value| value.parse::().ok()) +} + +/// 检测 root 或 Magisk 痕迹。 +fn has_root_marker() -> bool { + if read_real_uid() == Some(0) { + return true; + } + [ + "/system/app/Superuser.apk", + "/system/xbin/su", + "/system/bin/su", + "/data/local/bin/su", + "/sbin/su", + "/sbin/.magisk", + "/data/adb/magisk", + ] + .iter() + .any(|path| Path::new(path).exists()) +} + +/// 统计 /proc 数字目录数量。 +fn count_processes() -> jint { + let entries = match fs::read_dir("/proc") { + Ok(entries) => entries, + Err(_) => return -1, + }; + entries + .filter_map(Result::ok) + .filter_map(|entry| entry.file_name().into_string().ok()) + .filter(|name| !name.is_empty() && name.bytes().all(|byte| byte.is_ascii_digit())) + .count() as jint +} + +/// 创建 Java 字符串,失败时返回 null。 +fn new_java_string(env: JNIEnv<'_>, value: String) -> jstring { + match env.new_string(value) { + Ok(java_string) => java_string.into_raw(), + Err(_) => ptr::null_mut(), + } +} + +/// 读取 Java 字符串,失败时返回空字符串。 +fn read_java_string(env: &mut JNIEnv<'_>, value: &JString<'_>) -> String { + // JNI 字符串读取失败时降级为空字符串,避免 native 层 panic。 + env.get_string(value) + .map(|java_string| java_string.to_string_lossy().into_owned()) + .unwrap_or_default() +} + +/// Rust Native 骨架库加载入口。 +#[no_mangle] +pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *mut c_void) -> jint { + // 注册成功后声明 JNI 1.6;失败时返回 0,让 System.loadLibrary 明确失败。 + match register_native_methods(&vm) { + Ok(()) => JNI_VERSION_1_6, + Err(_) => 0, + } +} + +/// 返回 Rust Native 骨架是否可用。 +extern "system" fn native_is_available(_env: JNIEnv<'_>, _class: JClass<'_>) -> jboolean { + // 能执行到这里说明 so 已加载、JNI 动态注册已完成。 + JNI_TRUE +} + +/// 返回 Rust Native 骨架版本号。 +extern "system" fn native_version_code(_env: JNIEnv<'_>, _class: JClass<'_>) -> jint { + // 版本号用于 Kotlin 层和后续迁移任务做能力探测。 + FW_RUST_VERSION_CODE +} + +/// 返回当前进程 OOM adj。 +extern "system" fn native_get_oom_adj(_env: JNIEnv<'_>, _class: JClass<'_>) -> jint { + // 与 C++ `get_oom_adj` 保持同一默认值和旧路径换算逻辑。 + read_oom_adj() +} + +/// 返回系统内存信息:[总内存 KB, 空闲内存 KB, 可用内存 KB]。 +extern "system" fn native_get_memory_info(env: JNIEnv<'_>, _class: JClass<'_>) -> jlongArray { + // 数组结构与现有 C++ `getMemoryInfo` 保持一致。 + let values = read_memory_info(); + match env.new_long_array(values.len() as jint) { + Ok(array) => { + if env.set_long_array_region(&array, 0, &values).is_ok() { + array.into_raw() + } else { + ptr::null_mut() + } + } + Err(_) => ptr::null_mut(), + } +} + +/// 返回当前进程关键状态。 +extern "system" fn native_get_process_status(env: JNIEnv<'_>, _class: JClass<'_>) -> jstring { + // Kotlin 层直接展示该字符串,因此保持 C++ 过滤字段一致。 + new_java_string(env, read_process_status()) +} + +/// 返回 root / Magisk 痕迹检测结果。 +extern "system" fn native_check_root(_env: JNIEnv<'_>, _class: JClass<'_>) -> jboolean { + // Rust 层不执行提权命令,只做只读文件和 UID 检测。 + if has_root_marker() { + JNI_TRUE + } else { + JNI_FALSE + } +} + +/// 返回系统进程数量。 +extern "system" fn native_get_process_count(_env: JNIEnv<'_>, _class: JClass<'_>) -> jint { + // 与 C++ 逻辑一致,仅统计 /proc 下纯数字目录。 + count_processes() +} + +/// Rust MediaRoute 初始化。 +extern "system" fn native_media_route_init(_env: JNIEnv<'_>, _class: JClass<'_>) { + // 初始化只重置 Rust 侧服务状态,不操作 WakeLock。 + media_route_state().init(); +} + +/// Rust MediaRoute WakeLock 检查。 +extern "system" fn native_media_route_check_wake_lock(_env: JNIEnv<'_>, _class: JClass<'_>) { + // Kotlin 层仍负责真实 WakeLock 获取,Rust 只记录状态心跳。 + media_route_state().check_wake_lock(); +} + +/// Rust MediaRouteProviderService 启动通知。 +extern "system" fn native_media_route_on_service_started( + mut env: JNIEnv<'_>, + _class: JClass<'_>, + package_name: JString<'_>, + service_name: JString<'_>, +) { + let package = read_java_string(&mut env, &package_name); + let service = read_java_string(&mut env, &service_name); + media_route_state().on_service_started(package, service); +} + +/// Rust MediaRouteProviderService 停止通知。 +extern "system" fn native_media_route_on_service_stopped(_env: JNIEnv<'_>, _class: JClass<'_>) { + media_route_state().on_service_stopped(); +} + +/// Rust MediaRoute2ProviderService 启动通知。 +extern "system" fn native_media_route_on_service2_started( + mut env: JNIEnv<'_>, + _class: JClass<'_>, + package_name: JString<'_>, + service_name: JString<'_>, +) { + let package = read_java_string(&mut env, &package_name); + let service = read_java_string(&mut env, &service_name); + media_route_state().on_service2_started(package, service); +} + +/// Rust MediaRoute2ProviderService 停止通知。 +extern "system" fn native_media_route_on_service2_stopped(_env: JNIEnv<'_>, _class: JClass<'_>) { + media_route_state().on_service2_stopped(); +} + +/// Rust MediaRoute 心跳。 +extern "system" fn native_media_route_perform_heartbeat( + _env: JNIEnv<'_>, + _class: JClass<'_>, +) -> jboolean { + if media_route_state().perform_heartbeat() { + JNI_TRUE + } else { + JNI_FALSE + } +} + +/// Rust MediaRoute 服务状态。 +extern "system" fn native_media_route_get_service_status( + _env: JNIEnv<'_>, + _class: JClass<'_>, +) -> jint { + media_route_state().service_status() +} diff --git "a/\345\274\200\345\217\221\350\256\241\345\210\222.md" "b/\345\274\200\345\217\221\350\256\241\345\210\222.md" index bbee53a..4054260 100644 --- "a/\345\274\200\345\217\221\350\256\241\345\210\222.md" +++ "b/\345\274\200\345\217\221\350\256\241\345\210\222.md" @@ -6,6 +6,211 @@ --- +## 2026-05-28 阶段二:Rust 重构第一阶段 + +### 需求概述 + +在当前 C++ JNI 能力保持不变的前提下,新增 Rust Native 构建骨架,为后续迁移 JNI 外层、字符串保护、MediaRoute、进程/Socket 与 startActivity 编排做工程准备。 + +### 复杂度评估 + +级别:🟡 Standard +依赖模式:Linear + +### 三轮拆解记录 + +| 轮次 | 拆解层级 | 结果 | +|------|----------|------| +| 第 1 轮 | 迁移边界 | 本阶段只接 Rust so 骨架,不替换 `fw_native` / `fw_mediaroute` 行为 | +| 第 2 轮 | 构建链路 | Gradle 增加 `-PfwBuildRust=true` 可选开关,生成 4 ABI `libfw_rust.so` | +| 第 3 轮 | 验收顺序 | 先验证默认 Android 构建不受影响,再验证 Rust 工具链和可选 so 打包 | + +### 模块拆解 + +| ID | 模块 | 描述 | 改动文件 | 依赖 | 状态 | +|----|------|------|---------|------|------| +| R1 | Rust crate 骨架 | 新增 `fw_rust` cdylib、JNI 动态注册、能力探测函数 | `framework/src/main/rust/fw_rust/*` | 无 | ✅ | +| R2 | Gradle 构建接入 | 增加 4 ABI Rust 构建、复制到 generated jniLibs、默认关闭构建开关 | `framework/build.gradle.kts`, `.gitignore` | R1 | ✅ | +| R3 | Kotlin 内部桥 | 新增内部 `FwRustNative`,仅探测可选 so,不接主链路 | `framework/src/main/java/com/service/framework/rust/FwRustNative.kt` | R1-R2 | ✅ | +| R4 | 文档与测试 | README、开发计划、自动化测试计划记录 Rust 构建方式 | `README*`, `开发计划.md`, `自动化测试计划.md` | R1-R3 | ✅ | + +### DAG 依赖图 + +```mermaid +graph LR + R1 --> R2 + R2 --> R3 + R3 --> R4 +``` + +### 执行记录 + +| 波次 | Agent | 模块 | 结果 | 备注 | +|------|-------|------|------|------| +| 1 | Codex | R1-R3 | 完成 | 新增 Rust crate、Gradle 任务、Kotlin 内部桥 | +| 2 | Codex | R4 | 完成 | README 多语言入口、开发计划、自动化测试计划、CHANGELOG 已同步 | +| QA | Codex | 全部 | 通过 | 默认构建与可选 Rust Release 构建均通过 | + +### 变更审计日志 + +| 时间 | 操作 | 变更内容 | 操作者 | +|------|------|---------|--------| +| 2026-05-28 18:08:19 CST | 创建计划 | 新增 Rust 重构第一阶段计划 | Codex | +| 2026-05-28 18:27:45 CST | 代码实现 | 新增 `fw_rust`、`FwRustNative`、Gradle Rust 4 ABI 构建任务 | Codex | +| 2026-05-28 18:27:45 CST | 构建验证 | `./gradlew clean build` 与 `./gradlew :framework:assembleRelease -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` 通过 | Codex | +| 2026-05-28 18:27:45 CST | 产物审计 | AAR 包含 4 ABI `libfw_rust.so`,动态导出仅 `JNI_OnLoad`,LOAD Align 为 `0x4000` | Codex | + +### 当前进度 + +总进度:4/4 (100%) + +--- + +## 2026-05-28 阶段二:Rust 进程信息迁移 + +### 需求概述 + +在 Rust Native 骨架稳定后,先迁移低风险只读系统信息能力,包括 OOM adj、系统内存、进程状态、root 痕迹和进程数量,为后续迁移 JNI 外层积累可验证闭环。 + +### 复杂度评估 + +级别:🟡 Standard +依赖模式:Linear + +### 三轮拆解记录 + +| 轮次 | 拆解层级 | 结果 | +|------|----------|------| +| 第 1 轮 | 风险边界 | 只迁移只读 `/proc` 能力,不触碰 Binder、Parcel、fork、Socket、startActivity | +| 第 2 轮 | 接口边界 | Rust 动态注册新增进程探测方法,Kotlin 通过内部桥读取快照 | +| 第 3 轮 | 接入方式 | `FwNative` 摘要函数与健康巡检优先读取 Rust 快照,不存在 Rust so 时自动回退 | + +### 模块拆解 + +| ID | 模块 | 描述 | 改动文件 | 依赖 | 状态 | +|----|------|------|---------|------|------| +| P1 | Rust 只读进程探测 | 新增 OOM、内存、进程状态、root、进程数量 JNI 方法 | `framework/src/main/rust/fw_rust/src/lib.rs` | Rust 骨架 | ✅ | +| P2 | Kotlin 快照桥 | 新增 `FwRustProcessSnapshot` 与安全读取封装 | `FwRustNative.kt` | P1 | ✅ | +| P3 | 健康巡检接入 | Native 巡检日志融合 Rust 快照,缺失 Rust so 时回退 | `FwHealthMonitor.kt`, `FwNative.kt` | P2 | ✅ | + +### 执行记录 + +| 波次 | Agent | 模块 | 结果 | 备注 | +|------|-------|------|------|------| +| 1 | Codex | P1-P3 | 完成 | 受工具限制,本轮不派生子 Agent,主流程本地执行 | + +### 变更审计日志 + +| 时间 | 操作 | 变更内容 | 操作者 | +|------|------|---------|--------| +| 2026-05-28 18:36:38 CST | 迁移实现 | Rust 新增只读进程探测,Kotlin 健康巡检接入快照回退链路 | Codex | + +### 当前进度 + +总进度:3/3 (100%) + +--- + +## 2026-05-28 阶段二:Rust MediaRoute 迁移与模拟器测试 + +### 需求概述 + +继续迁移适合 Rust 替代的 JNI 层能力,将 MediaRoute native 服务状态、WakeLock 检查计数和心跳判断迁移到 Rust,并在完成后启动 Android 模拟器进行安装与基础运行验证。 + +### 复杂度评估 + +级别:🟡 Standard +依赖模式:Linear + +### 三轮拆解记录 + +| 轮次 | 拆解层级 | 结果 | +|------|----------|------| +| 第 1 轮 | 迁移范围 | 只迁移 MediaRoute 状态机和心跳,不迁移 Android WakeLock 获取本身 | +| 第 2 轮 | 回退策略 | Kotlin `FwMediaRouteNative` 优先 Rust,Rust 不可用时回退 C++ `fw_mediaroute` | +| 第 3 轮 | 验收路径 | 本地构建、Rust so 符号/对齐审计、模拟器安装与基础启动验证 | + +### 模块拆解 + +| ID | 模块 | 描述 | 改动文件 | 依赖 | 状态 | +|----|------|------|---------|------|------| +| M1 | Rust MediaRoute 状态机 | 新增 Rust 全局状态、服务启停、心跳与状态码 JNI 方法 | `framework/src/main/rust/fw_rust/src/lib.rs` | Rust 骨架 | ✅ | +| M2 | Kotlin 回退接入 | 新增 `FwRustMediaRouteNative`,`FwMediaRouteNative` 改为 Rust 优先/C++ 回退 | `FwRustMediaRouteNative.kt`, `FwMediaRouteNative.kt` | M1 | ✅ | +| M3 | 模拟器验收 | 构建后启动模拟器,安装 App,执行基础运行与日志验证 | Gradle/ADB/Emulator | M1-M2 | ✅ | + +### 执行记录 + +| 波次 | Agent | 模块 | 结果 | 备注 | +|------|-------|------|------|------| +| 1 | Codex | M1-M2 | 完成 | Rust MediaRoute 状态机与 Kotlin 回退链路已完成 | +| 2 | Codex | M3 | 完成 | Android 16 模拟器安装、启动、日志与 connectedAndroidTest 已验证 | + +### 变更审计日志 + +| 时间 | 操作 | 变更内容 | 操作者 | +|------|------|---------|--------| +| 2026-05-28 18:45:29 CST | 迁移实现 | 新增 Rust MediaRoute 状态机与 Kotlin Rust 优先回退入口 | Codex | +| 2026-05-28 18:54:45 CST | 本地验证 | `cargo fmt --check`、`git diff --check`、`./gradlew :framework:lintRelease`、`./gradlew clean build`、`./gradlew :framework:assembleAndroidTest :app:assembleAndroidTest` 均通过 | Codex | +| 2026-05-28 18:54:45 CST | Native 审计 | Rust AAR 产物包含 4 ABI `libfw_rust.so`,动态导出仅 `JNI_OnLoad`,LOAD Align 均为 `0x4000` | Codex | +| 2026-05-28 18:59:52 CST | 模拟器验证 | `medium_phone` Android 16 / arm64-v8a 启动成功,带 Rust debug APK 安装成功,`SplashActivity` 可启动,Rust MediaRoute 初始化日志正常 | Codex | +| 2026-05-28 19:00:45 CST | 设备侧测试 | 首轮 `connectedAndroidTest` 验证了安装和基础链路;后续已补齐真实 instrumentation 用例并在 Android 16 模拟器复验通过 | Codex | + +### 当前进度 + +总进度:3/3 (100%) + +--- + +## 2026-05-28 阶段二:Rust Native 真实设备测试与打包边界收尾 + +### 需求概述 + +对 Rust JNI 第一阶段做发布前收尾:补齐真实设备侧 instrumentation 测试,验证默认 C++ 回退与可选 Rust 打包两套矩阵,并修复默认构建误打包历史 `libfw_rust.so` 的边界问题。 + +### 复杂度评估 + +级别:🟡 Standard +依赖模式:Fan-in + +### 三轮拆解记录 + +| 轮次 | 拆解层级 | 结果 | +|------|----------|------| +| 第 1 轮 | 测试入口 | framework 覆盖 Rust 进程快照、Rust MediaRoute 状态机、公共 MediaRoute 后端迁移;app 覆盖 Native startActivity 策略 | +| 第 2 轮 | 构建矩阵 | 默认构建必须只打包 C++ so;显式 `-PfwBuildRust=true` 才打包 4 ABI `libfw_rust.so` | +| 第 3 轮 | 设备验证 | Android 16 / arm64-v8a 模拟器分别执行默认 C++ 回退和 Rust 打包 `connectedAndroidTest` | + +### 模块拆解 + +| ID | 模块 | 描述 | 改动文件 | 依赖 | 状态 | +|----|------|------|---------|------|------| +| T1 | Rust instrumentation | 新增 Rust 进程探测、MediaRoute 状态机、公共入口后端迁移测试 | `framework/src/androidTest/java/com/service/framework/rust/FwRustNativeInstrumentedTest.kt` | Rust Native | ✅ | +| T2 | startActivity instrumentation | 新增 C++ `start/` 真实触发测试,覆盖稳定策略、受限策略和枚举完整性 | `app/src/androidTest/java/com/google/services/FwStartInstrumentedTest.kt` | C++ start | ✅ | +| T3 | Debug 测试 Activity | 新增 debug-only 极简 Activity,避免 Compose 动画影响 instrumentation idle | `app/src/debug/*` | T2 | ✅ | +| T4 | 打包边界修复 | `jniLibs` 生成目录只在 `fwBuildRust=true` 时加入 variant source | `framework/build.gradle.kts` | Rust Gradle | ✅ | +| T5 | 文档记录 | 同步开发计划与自动化测试计划,记录两套矩阵结果 | `开发计划.md`, `自动化测试计划.md` | T1-T4 | ✅ | + +### 验证记录 + +| 验证项 | 命令/方式 | 结果 | +|--------|-----------|------| +| Rust 格式 | `PATH=/opt/homebrew/opt/rustup/bin:$PATH cargo fmt --check` | ✅ | +| 默认完整构建 | `./gradlew clean build` | ✅ | +| AndroidTest 构建 | `./gradlew :framework:assembleAndroidTest :app:assembleAndroidTest -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` | ✅ | +| Rust 构建矩阵 | `./gradlew :framework:assembleRelease :app:assembleDebug -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` | ✅ | +| 默认构建矩阵 | `./gradlew :framework:assembleRelease :app:assembleDebug --rerun-tasks` | ✅ | +| 默认产物审计 | 检查 APK/AAR 中 `libfw_rust.so` | ✅ 未误打包 | +| Rust 产物审计 | 检查 APK/AAR 中 `libfw_rust.so` | ✅ 4 ABI 均存在 | +| 默认 C++ 回退测试 | `./gradlew :app:connectedAndroidTest --rerun-tasks` 与 `./gradlew :framework:connectedAndroidTest --rerun-tasks` | ✅ | +| Rust 打包测试 | `./gradlew connectedAndroidTest --rerun-tasks -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` | ✅ app 5 tests / framework 3 tests 全通过 | +| 崩溃日志扫描 | `adb -s emulator-5554 logcat -d` 关键错误扫描 | ✅ 无 AndroidRuntime/JNI/native crash | + +### 当前进度 + +总进度:5/5 (100%) + +--- + ## 2026-05-28 阶段二:全量保活策略优化升级 ### 需求概述 @@ -402,4 +607,4 @@ graph LR *开发计划版本: v2.0.1* *创建时间: 2026-02-20* -*最后更新: 2026-02-20 (所有任务已完成)* +*最后更新: 2026-05-28 (Rust Native 真实设备测试与打包边界收尾完成)* diff --git "a/\350\207\252\345\212\250\345\214\226\346\265\213\350\257\225\350\256\241\345\210\222.md" "b/\350\207\252\345\212\250\345\214\226\346\265\213\350\257\225\350\256\241\345\210\222.md" index 5a5f589..1caa0ae 100644 --- "a/\350\207\252\345\212\250\345\214\226\346\265\213\350\257\225\350\256\241\345\210\222.md" +++ "b/\350\207\252\345\212\250\345\214\226\346\265\213\350\257\225\350\256\241\345\210\222.md" @@ -10,6 +10,8 @@ | framework Debug AAR | `./gradlew :framework:assembleDebug` | Kotlin、Manifest、CMake、四架构 native 均构建成功 | | app Debug APK | `./gradlew :app:assembleDebug` | 示例应用可安装 | | 发布前检查 | `./gradlew :framework:assembleRelease` | release AAR、sources、javadoc 任务可执行 | +| Rust 工具链检查 | `./gradlew :framework:checkFwRustToolchain` | Cargo 与 NDK 四架构 linker 可用 | +| Rust Native 可选打包 | `./gradlew :framework:assembleRelease -PfwBuildRust=true` | 生成并打包 4 ABI `libfw_rust.so` | ## 2. Native startActivity 验证 @@ -63,3 +65,56 @@ adb logcat | grep -E "Fw|FwStart|FwHealth|FwVpnService|FwMediaBrowser" - `FwStart` 每个策略都有开始、成功、失败或跳过日志。 - VPN 未授权时只提示授权,不尝试建立隧道。 - MediaBrowserService 创建、连接、销毁都有状态打点。 + +## 7. Rust 重构第一阶段验证记录 + +| 验证项 | 命令/方式 | 结果 | +|--------|-----------|------| +| 默认完整构建 | `./gradlew clean build` | 通过 | +| AndroidTest 构建 | `./gradlew :framework:assembleAndroidTest :app:assembleAndroidTest` | 通过 | +| Release Lint | `./gradlew :framework:lintRelease :app:lintRelease` | 通过 | +| Rust 工具链 | `./gradlew :framework:checkFwRustToolchain -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` | 通过 | +| Rust Release 打包 | `./gradlew :framework:assembleRelease -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` | 通过 | +| AAR 内容 | `unzip -l framework/build/outputs/aar/framework-release.aar` | 4 ABI 均包含 `libfw_rust.so` | +| Rust so 符号 | `llvm-nm -D --defined-only libfw_rust.so` | 动态导出仅 `JNI_OnLoad` | +| 16KB 对齐 | `llvm-readelf -l libfw_rust.so` | 4 ABI LOAD Align 均为 `0x4000` | +| 默认 C++ 回退设备侧测试 | `./gradlew :framework:connectedAndroidTest --rerun-tasks` | 通过;Rust 专属用例在未打包 Rust so 时受控跳过,公共 MediaRoute 入口走 C++ 回退 | + +## 8. Rust 进程信息迁移验证记录 + +| 验证项 | 命令/方式 | 预期 | +|--------|-----------|------| +| Rust 代码格式 | `cargo fmt --manifest-path framework/src/main/rust/fw_rust/Cargo.toml -- --check` | 通过 | +| Rust 4 ABI 编译 | `./gradlew :framework:assembleRelease -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` | 通过 | +| 默认回退构建 | `./gradlew :framework:assembleRelease` | 不依赖 Rust so,构建通过 | +| 健康巡检回退 | 默认 AAR 不含 `libfw_rust.so` 时调用 `Fw.check()` | 只输出调试日志,不影响 C++ Native 巡检 | +| 符号防护 | `llvm-nm -D --defined-only libfw_rust.so` | 只导出 `JNI_OnLoad` | + +本轮补充验证:`./gradlew clean build` 已单独重跑通过;`./gradlew :framework:assembleAndroidTest :app:assembleAndroidTest -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` 已通过;Android 16 模拟器已执行默认 C++ 回退矩阵和 Rust 打包矩阵。 + +## 9. Rust MediaRoute 迁移与模拟器验证记录 + +| 验证项 | 命令/方式 | 预期 | +|--------|-----------|------| +| Rust MediaRoute 编译 | `./gradlew :framework:assembleRelease -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` | 通过且无 Rust 编译告警 | +| 默认 C++ 回退 | `./gradlew :framework:assembleRelease` | 不依赖 Rust so,构建通过 | +| 符号防护 | `llvm-nm -D --defined-only libfw_rust.so` | 只导出 `JNI_OnLoad` | +| 模拟器启动 | `android emulator start medium_phone` 失败后,使用 `emulator -avd medium_phone -no-snapshot-load -no-snapshot-save -no-boot-anim -no-window` | Android 16 / arm64-v8a 设备进入 `device` 状态 | +| App 安装 | `./gradlew :app:installDebug -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` | Debug APK 安装成功,APK 内包含 4 ABI `libfw_rust.so` | +| 基础运行 | `adb shell am start -n com.google.services/.SplashActivity` | App 可启动,Rust MediaRoute 初始化成功,logcat 无 JNI/native/AndroidRuntime 崩溃 | +| 默认 C++ 回退设备侧测试 | `./gradlew :app:connectedAndroidTest --rerun-tasks` 与 `./gradlew :framework:connectedAndroidTest --rerun-tasks` | App 5 个 startActivity 用例通过;framework 公共入口用例通过,Rust 专属用例在默认构建受控跳过 | +| Rust 打包设备侧测试 | `./gradlew connectedAndroidTest --rerun-tasks -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo` | app 5 tests / 0 failures / 0 skipped;framework 3 tests / 0 failures / 0 skipped | +| 崩溃日志扫描 | `adb -s emulator-5554 logcat -d | rg -i 'AndroidRuntime|FATAL EXCEPTION|UnsatisfiedLinkError|JNI DETECTED ERROR|Fatal signal|SIGSEGV|SIGABRT'` | 无应用崩溃、JNI 错误或 native crash | +| 默认产物边界 | 默认 `:framework:assembleRelease` 与 `:app:assembleDebug` 后检查 APK/AAR | 未包含 `libfw_rust.so`,不会误打包旧 Rust so | +| Rust 产物边界 | 带 `-PfwBuildRust=true` 构建后检查 APK/AAR | APK/AAR 均包含 4 ABI `libfw_rust.so` | + +本轮补充验证:`cargo fmt --check`、`./gradlew clean build`、`./gradlew :framework:assembleAndroidTest :app:assembleAndroidTest -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo`、`./gradlew :framework:assembleRelease :app:assembleDebug -PfwBuildRust=true -PfwCargoPath=/opt/homebrew/opt/rustup/bin/cargo`、默认 `./gradlew :framework:assembleRelease :app:assembleDebug --rerun-tasks`、默认 C++ 回退 `connectedAndroidTest`、带 Rust `connectedAndroidTest` 均已通过。`android emulator start medium_phone` 首次受快照影响退出,已通过禁用快照的命令完成 Android 16 / arm64-v8a 模拟器验证。 + +## 10. 真实设备侧用例覆盖 + +| 测试类 | 用例数 | 覆盖范围 | 最终结果 | +|--------|--------|----------|----------| +| `FwStartInstrumentedTest` | 5 | `modeMask=0`、Application Context 稳定策略、Activity Context 稳定策略、系统限制策略受控结果、所有 `FwStartStrategy` 枚举覆盖 | 5 tests / 0 failures / 0 skipped | +| `FwRustNativeInstrumentedTest` | 3 | Rust `/proc` 进程快照、Rust MediaRoute 状态机、公共 MediaRoute Rust/C++ 后端状态迁移 | 3 tests / 0 failures / 0 skipped | + +Debug 包新增 `TestStartActivity` 只用于 instrumentation 测试,避免示例 `MainActivity` 的 Compose 动画影响 `startActivitySync` 空闲等待;该 Activity 位于 debug 源集,不进入 release 产物。