diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a7d9519..302ddbbf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,10 +1,12 @@ name: CI Build on: - workflow_dispatch: - pull_request: push: + branches: [ master ] + pull_request: + branches: [ master ] merge_group: + workflow_dispatch: jobs: build: @@ -24,6 +26,82 @@ jobs: submodules: 'recursive' fetch-depth: 0 + - name: System Port + run: | + mkdir -p out/Port1 + + javac -source 1.8 -target 1.8 \ + -bootclasspath $ANDROID_HOME/platforms/android-34/android.jar \ + -d out/Port1 \ + src/android/os/SharedMemory.java + + $ANDROID_HOME/build-tools/34.0.0/d8 \ + --output out/Port1/output.zip \ + out/Port1/android/os/SharedMemory.class + + unzip -o out/Port1/output.zip -d out/Port1/ + ls -lah out/Port1/classes.dex + + - name: System Port File + uses: actions/upload-artifact@v5 + with: + name: Port-System-${{ env.commit }} + path: out/Port1 + retention-days: 7 + + - name: Fix Submodule sun.misc + run: | + echo "Searching in submodules..." + TARGET_FILE=$(find . -name "CompoundEnumeration.java" -path "*/sun/misc/*" | head -n 1) + + if [ -n "$TARGET_FILE" ]; then + echo "Target found: $TARGET_FILE" + BASE_JAVA_DIR=$(echo "$TARGET_FILE" | sed 's|sun/misc/CompoundEnumeration.java||') + NEW_DIR="${BASE_JAVA_DIR}internal/stubs" + + mkdir -p "$NEW_DIR" + mv "$TARGET_FILE" "$NEW_DIR/" + sed -i 's|package sun.misc;|package internal.stubs;|g' "$NEW_DIR/CompoundEnumeration.java" + + echo "Syncing imports..." + find . -type f -name "*.java" -exec sed -i 's|import sun.misc.CompoundEnumeration;|import internal.stubs.CompoundEnumeration;|g' {} + + find . -type f -name "*.java" -exec sed -i 's|sun.misc.CompoundEnumeration|internal.stubs.CompoundEnumeration|g' {} + + + rm -rf "${BASE_JAVA_DIR}sun" + echo "Submodule fixed successfully!" + else + echo "File not found. Check if submodules are checked out correctly." + fi + + - name: Hot-fix sun.net.www in Submodule + run: | + TARGET_FILE="core/core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java" + + if [ -f "$TARGET_FILE" ]; then + echo "Problem seen" + sed -i 's/sun\.net\.www/sunp\.net\.www/g' "$TARGET_FILE" + STUB_DIR="core/core/src/main/java/sunp/net/www/protocol/jar" + mkdir -p "$STUB_DIR" + + echo -e "package sunp.net.www;\npublic class ParseUtil { public static String encodePath(String p, boolean b) { return p; } }" > "core/core/src/main/java/sunp/net/www/ParseUtil.java" + + echo -e "package sunp.net.www.protocol.jar;\nimport java.net.URLStreamHandler;\npublic class Handler extends URLStreamHandler { protected java.net.URLConnection openConnection(java.net.URL u) throws java.io.IOException { return null; } }" > "$STUB_DIR/Handler.java" + + echo "Successfully patched" + fi + + - name: Brute Force Git Repair + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git checkout -B master + git fetch --tags --force + mkdir -p .git/refs/heads + echo $(git rev-parse HEAD) > .git/refs/heads/master + if [ -z "$(git tag)" ]; then + git tag 1.0.2 + fi + git update-ref refs/heads/master $(git rev-parse HEAD) + - name: 設定快取 uses: actions/cache@v4 with: @@ -43,15 +121,27 @@ jobs: echo ${{ secrets.KEY_STORE }} | base64 --decode > key.jks fi + - name: 檢出 libxposed/api + uses: actions/checkout@main + with: + repository: libxposed/api + path: libxposed/api + + - name: 檢出 libxposed/service + uses: actions/checkout@main + with: + repository: libxposed/service + path: libxposed/service + - name: 設定 Java uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '17' distribution: 'zulu' - name: 設定 Gradle uses: gradle/actions/setup-gradle@v5 - + - name: 設定 Android SDK uses: android-actions/setup-android@v3 @@ -70,7 +160,7 @@ jobs: run: rm -rf "$ANDROID_HOME/cmake" - name: 授予 gradlew 執行權限 - run: chmod +x gradlew + run: chmod +x gradlew - name: 使用 Gradle 構建依賴項 working-directory: libxposed @@ -84,7 +174,7 @@ jobs: - name: 設定 commit id run: echo "commit=$(echo ${{ github.sha }} | cut -c-7)" > $GITHUB_ENV - + - name: 使用 Gradle 構建 run: ./gradlew buildAll @@ -115,4 +205,4 @@ jobs: with: name: symbols-${{ env.commit }} path: | - patch-loader/build/symbols \ No newline at end of file + patch-loader/build/symbols diff --git a/.github/workflows/sync-fork.yml b/.github/workflows/sync-fork.yml new file mode 100644 index 00000000..be4963da --- /dev/null +++ b/.github/workflows/sync-fork.yml @@ -0,0 +1,64 @@ +name: Sync Fork (Keep My Code) + +on: + schedule: + - cron: '0 */6 * * *' + workflow_dispatch: + +permissions: + contents: write + +jobs: + sync: + name: Sync with Upstream (Keep My Changes) + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.PAT_TOKEN }} + + - name: Get upstream repo URL + id: upstream + env: + GH_TOKEN: ${{ secrets.PAT_TOKEN }} + run: | + UPSTREAM=$(gh api repos/${{ github.repository }} --jq '.parent.full_name') + if [ -z "$UPSTREAM" ] || [ "$UPSTREAM" = "null" ]; then + exit 1 + fi + echo "repo=$UPSTREAM" >> $GITHUB_OUTPUT + + - name: Config Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Force Merge Upstream + id: merge + run: | + git remote add upstream https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/${{ steps.upstream.outputs.repo }}.git + git fetch upstream open-source + + BEFORE=$(git rev-parse HEAD) + + if ! git merge upstream/open-source --allow-unrelated-histories -X ours --no-edit; then + echo "Conflicts detected, forcing our versions..." + git checkout --ours . + git add . + git commit -m "Sync: Force resolve conflicts using master branch code" + fi + + AFTER=$(git rev-parse HEAD) + if [ "$BEFORE" != "$AFTER" ]; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + - name: Push to Master + if: steps.merge.outputs.changed == 'true' + run: | + git push https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/${{ github.repository }}.git master diff --git a/.gitmodules b/.gitmodules index 40be1dc5..8ed8e57a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,7 @@ path = core url = https://github.com/HSSkyBoy/Vector.git branch = master +[submodule "libxposed"] + path = libxposed + url = https://github.com/7723mod/libxposed.git + branch = main diff --git a/README.md b/README.md index 0f61e79b..5b7d8873 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ We sincerely invite you to join our [Telegram](https://t.me/NPatch) group to get ## Supported Versions -- Min: Android 9 +- Min: Android 7 - Max: In theory, same with [JingMatrix/LSPosed](https://github.com/JingMatrix/LSPosed#supported-versions) ## Download diff --git a/build.gradle.kts b/build.gradle.kts index 912a88bc..e6944330 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,8 @@ -import com.android.build.api.dsl.ApplicationExtension -import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.gradle.BaseExtension +import com.android.build.gradle.LibraryExtension import org.eclipse.jgit.api.Git import org.eclipse.jgit.internal.storage.file.FileRepository import org.eclipse.jgit.storage.file.FileRepositoryBuilder -import com.android.build.gradle.LibraryExtension import org.gradle.kotlin.dsl.extra plugins { @@ -35,31 +33,25 @@ val (coreCommitCount, coreLatestTag) = FileRepositoryBuilder().setGitDir(rootPro .runCatching { build().use { repo -> val git = Git(repo) - val coreCommitCount = - git.log() - .add(repo.refDatabase.exactRef("HEAD").objectId) - .call().count() - val ver = git.describe() - .setTags(true) - .setAbbrev(0).call().removePrefix("v") + val coreCommitCount = git.log().add(repo.refDatabase.exactRef("HEAD").objectId).call().count() + val ver = git.describe().setTags(true).setAbbrev(0).call().removePrefix("v") coreCommitCount to ver } }.getOrNull() ?: (3015 to "2.0") -// sync from https://github.com/JingMartix/LSPosed/blob/master/build.gradle.kts val defaultManagerPackageName by extra("org.lsposed.npatch") val apiCode by extra(100) val verCode by extra(commitCount) -val verName by extra("1.0.2") +val verName by extra("0.8.0") val coreVerCode by extra(coreCommitCount) val coreVerName by extra(coreLatestTag) -val androidMinSdkVersion by extra(28) -val androidTargetSdkVersion by extra(37) -val androidCompileSdkVersion by extra(37) +val androidMinSdkVersion by extra(24) +val androidTargetSdkVersion by extra(36) +val androidCompileSdkVersion by extra(36) val androidCompileNdkVersion by extra("29.0.13599879") -val androidBuildToolsVersion by extra("37.0.0") -val androidSourceCompatibility by extra(JavaVersion.VERSION_21) -val androidTargetCompatibility by extra(JavaVersion.VERSION_21) +val androidBuildToolsVersion by extra("36.1.0") +val androidSourceCompatibility by extra(JavaVersion.VERSION_17) +val androidTargetCompatibility by extra(JavaVersion.VERSION_17) tasks.register("clean") { delete(layout.buildDirectory) @@ -67,7 +59,6 @@ tasks.register("clean") { listOf("Debug", "Release").forEach { variant -> tasks.register("build$variant") { - description = "Build NPatch with $variant" dependsOn(tasks.findByPath(":jar:build$variant") ?: "jar:build$variant") dependsOn(tasks.findByPath(":manager:build$variant") ?: "manager:build$variant") } @@ -80,8 +71,8 @@ tasks.register("buildAll") { fun Project.configureBaseExtension() { extensions.findByType(BaseExtension::class)?.run { compileSdkVersion(androidCompileSdkVersion) - ndkVersion = androidCompileNdkVersion buildToolsVersion = androidBuildToolsVersion + ndkVersion = androidCompileNdkVersion externalNativeBuild.cmake { version = "3.29.8+" @@ -93,22 +84,13 @@ fun Project.configureBaseExtension() { targetSdk = androidTargetSdkVersion versionCode = verCode versionName = verName - - signingConfigs.create("config") { - val androidStoreFile = project.findProperty("androidStoreFile") as String? - if (!androidStoreFile.isNullOrEmpty()) { - storeFile = rootProject.file(androidStoreFile) - storePassword = project.property("androidStorePassword") as String - keyAlias = project.property("androidKeyAlias") as String - keyPassword = project.property("androidKeyPassword") as String - } - } + multiDexEnabled = true externalNativeBuild { cmake { arguments += "-DVECTOR_ROOT=${File(rootDir.absolutePath, "core")}" arguments += "-DEXTERNAL_ROOT=${File(rootDir.absolutePath, "core/external")}" - arguments += "-DCORE_ROOT=${File(rootDir.absolutePath, "core/native") }" + arguments += "-DCORE_ROOT=${File(rootDir.absolutePath, "core/core/src/main/jni")}" abiFilters("arm64-v8a", "x86_64") val flags = arrayOf( "-Wall", @@ -136,14 +118,12 @@ fun Project.configureBaseExtension() { } compileOptions { - targetCompatibility(androidTargetCompatibility) - sourceCompatibility(androidSourceCompatibility) + sourceCompatibility = androidSourceCompatibility + targetCompatibility = androidTargetCompatibility + isCoreLibraryDesugaringEnabled = true } - buildTypes { - all { - signingConfig = if (signingConfigs["config"].storeFile != null) signingConfigs["config"] else signingConfigs["debug"] - } + buildTypes { named("debug") { externalNativeBuild { cmake { @@ -189,61 +169,55 @@ fun Project.configureBaseExtension() { } } } - } + } } +} - extensions.findByType(ApplicationExtension::class)?.lint { - abortOnError = true - checkReleaseBuilds = false - } +subprojects { + plugins.withId("com.android.application") { configureBaseExtension() } + plugins.withId("com.android.library") { configureBaseExtension() } - extensions.findByType(ApplicationAndroidComponentsExtension::class)?.let { androidComponents -> - val optimizeReleaseRes = task("optimizeReleaseRes").doLast { - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val aapt2Name = if (isWindows) "aapt2.exe" else "aapt2" - - val aapt2 = File( - androidComponents.sdkComponents.sdkDirectory.get().asFile, - "build-tools/${androidBuildToolsVersion}/$aapt2Name" - ) - val zip = java.nio.file.Paths.get( - project.buildDir.path, - "intermediates", - "optimized_processed_res", - "release", - "optimizeReleaseResources", - "resources-release-optimize.ap_" - ) - val optimized = File("${zip}.opt") - val cmd = exec { - commandLine( - aapt2, "optimize", - "--collapse-resource-names", - "--enable-sparse-encoding", - "-o", optimized, - zip - ) - isIgnoreExitValue = false - } - if (cmd.exitValue == 0) { - delete(zip) - optimized.renameTo(zip.toFile()) + afterEvaluate { + if (plugins.hasPlugin("com.android.application") || plugins.hasPlugin("com.android.library")) { + dependencies { + add("coreLibraryDesugaring", "com.android.tools:desugar_jdk_libs_nio:2.1.5") } } + } +} - tasks.configureEach { - if (name == "optimizeReleaseResources") { - finalizedBy(optimizeReleaseRes) - } +allprojects { + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } } + tasks.withType().configureEach { + sourceCompatibility = "17" + targetCompatibility = "17" + } } -subprojects { - plugins.withId("com.android.application") { - configureBaseExtension() - } - plugins.withId("com.android.library") { - configureBaseExtension() +project(":core") { + afterEvaluate { + if (property("android") is LibraryExtension) { + val android = property("android") as LibraryExtension + android.run { + buildTypes { + getByName("release") { + proguardFiles(rootProject.file("share/lspatch-rules.pro")) + } + } + } + } } } + +gradle.taskGraph.whenReady { + allTasks.forEach { task -> + if (task.name.contains("lint", ignoreCase = true)) { + task.enabled = false + } + } +} + diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index d17b5ba8..f960d959 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -21,6 +21,10 @@ android { applicationId = defaultManagerPackageName } + compileOptions { + isCoreLibraryDesugaringEnabled = true + } + androidResources { noCompress.add(".so") } @@ -39,8 +43,8 @@ android { buildTypes { release { - isMinifyEnabled = true // 启用 R8/ProGuard 进行代码压缩、优化和混淆。 - isShrinkResources = true // 启用资源缩减,移除未被引用的资源文件。 + isMinifyEnabled = false // 启用 R8/ProGuard 进行代码压缩、优化和混淆。 + isShrinkResources = false // 启用资源缩减,移除未被引用的资源文件。 isDebuggable = false // 发布版本禁止调试。 proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), @@ -149,4 +153,5 @@ dependencies { debugImplementation(npatch.androidx.compose.ui.tooling) debugImplementation(npatch.androidx.customview) debugImplementation(npatch.androidx.customview.poolingcontainer) + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.1.5") } diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml index c07f011c..5434fc18 100644 --- a/manager/src/main/AndroidManifest.xml +++ b/manager/src/main/AndroidManifest.xml @@ -98,4 +98,4 @@ android:enabled="true" /> - \ No newline at end of file + diff --git a/manager/src/main/java/nkbe/util/IntentSenderHelper.kt b/manager/src/main/java/nkbe/util/IntentSenderHelper.kt new file mode 100644 index 00000000..80450db1 --- /dev/null +++ b/manager/src/main/java/nkbe/util/IntentSenderHelper.kt @@ -0,0 +1,41 @@ +package nkbe.util + +import android.content.IIntentReceiver +import android.content.IIntentSender +import android.content.Intent +import android.content.IntentSender +import android.os.Bundle +import android.os.IBinder + +object IntentSenderHelper { + + fun newIntentSender(binder: IIntentSender): IntentSender { + return IntentSender::class.java.getConstructor(IIntentSender::class.java).newInstance(binder) + } + + class IIntentSenderAdaptor(private val listener: (Intent) -> Unit) : IIntentSender.Stub() { + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + finishedReceiver: IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ): Int { + listener(intent) + return 0 + } + + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + whitelistToken: IBinder?, + finishedReceiver: IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ) { + listener(intent) + } + } +} diff --git a/manager/src/main/java/nkbe/util/NPackageManager.kt b/manager/src/main/java/nkbe/util/NPackageManager.kt new file mode 100644 index 00000000..4700affc --- /dev/null +++ b/manager/src/main/java/nkbe/util/NPackageManager.kt @@ -0,0 +1,238 @@ +package nkbe.util + +import android.R +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageInstallerHidden.SessionParamsHidden +import android.content.pm.PackageManager +import android.content.pm.PackageManagerHidden +import android.net.Uri +import android.os.Parcelable +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import dev.rikka.tools.refine.Refine +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import me.zhanghai.android.appiconloader.AppIconLoader +import org.lsposed.npatch.config.ConfigManager +import org.lsposed.npatch.config.Configs +import org.lsposed.npatch.lspApp +import org.lsposed.npatch.share.Constants +import java.io.File +import java.io.IOException +import java.text.Collator +import java.util.* +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +object NPackageManager { + + private const val TAG = "LSPPackageManager" + private const val SETTINGS_CATEGORY = "de.robv.android.xposed.category.MODULE_SETTINGS" + + const val STATUS_USER_CANCELLED = -2 + + @Parcelize + class AppInfo(val app: ApplicationInfo, val label: String) : Parcelable { + val isXposedModule: Boolean + get() = app.metaData?.get("xposedminversion") != null + } + + var appList by mutableStateOf(listOf()) + private set + + @SuppressLint("StaticFieldLeak") + private val iconLoader = AppIconLoader(lspApp.resources.getDimensionPixelSize(R.dimen.app_icon_size), false, lspApp) + private val appIcon = mutableMapOf() + + + suspend fun fetchAppList() { + withContext(Dispatchers.IO) { + val pm = lspApp.packageManager + val collection = mutableListOf() + val applicationList: List + + if (ShizukuApi.isPermissionGranted) { + applicationList = runCatching { + ShizukuApi.getInstalledApplications() + }.getOrElse { + pm.getInstalledApplications(PackageManager.GET_META_DATA) + } + } else { + applicationList = pm.getInstalledApplications(PackageManager.GET_META_DATA) + } + + applicationList.forEach { + val label = pm.getApplicationLabel(it) + collection.add(AppInfo(it, label.toString())) + appIcon[it.packageName] = iconLoader.loadIcon(it).asImageBitmap() + } + + collection.sortWith(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) + val modules = buildMap { + collection.forEach { if (it.isXposedModule) put(it.app.packageName, it.app.sourceDir) } + } + ConfigManager.updateModules(modules) + appList = collection + } + } + + fun getIcon(appInfo: AppInfo) = appIcon[appInfo.app.packageName]!! + + suspend fun cleanTmpApkDir() { + withContext(Dispatchers.IO) { + lspApp.tmpApkDir.listFiles()?.forEach(File::delete) + } + } + + suspend fun cleanExternalTmpApkDir(){ + withContext(Dispatchers.IO) { + lspApp.externalCacheDir?.listFiles()?.forEach(File::delete) + } + } + + suspend fun install(): Pair { + var status = PackageInstaller.STATUS_FAILURE + var message: String? = null + withContext(Dispatchers.IO) { + runCatching { + val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) + try { + val hiddenParams = Refine.unsafeCast(params) + var flags = hiddenParams.installFlags + flags = flags or PackageManagerHidden.INSTALL_ALLOW_TEST or PackageManagerHidden.INSTALL_REPLACE_EXISTING + hiddenParams.installFlags = flags + } catch (e: Throwable) {} + + val session = ShizukuApi.createPackageInstallerSession(params) + + if (session == null) { + val uri = Configs.storageDirectory?.toUri() ?: throw IOException("Uri is null") + val root = DocumentFile.fromTreeUri(lspApp, uri) ?: throw IOException("DocumentFile is null") + root.listFiles().forEach { file -> + if (file.name?.endsWith(Constants.PATCH_FILE_SUFFIX) == true) { + val cacheFile = File(lspApp.cacheDir, file.name!!) + lspApp.contentResolver.openInputStream(file.uri)?.use { input -> + cacheFile.outputStream().use { output -> input.copyTo(output) } + } + ShizukuApi.installApkNormal(lspApp, cacheFile) + status = PackageInstaller.STATUS_PENDING_USER_ACTION + return@runCatching + } + } + } else { + session.use { s -> + val uri = Configs.storageDirectory?.toUri() ?: throw IOException("Uri is null") + val root = DocumentFile.fromTreeUri(lspApp, uri) ?: throw IOException("DocumentFile is null") + root.listFiles().forEach { file -> + if (file.name?.endsWith(Constants.PATCH_FILE_SUFFIX) != true) return@forEach + lspApp.contentResolver.openInputStream(file.uri)?.use { input -> + s.openWrite(file.name!!, 0, input.available().toLong()).use { output -> + input.copyTo(output) + s.fsync(output) + } + } + } + var result: Intent? = null + suspendCoroutine { cont -> + val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent -> + result = intent + cont.resume(Unit) + } + s.commit(IntentSenderHelper.newIntentSender(adapter)) + } + result?.let { + status = it.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + message = it.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + } + } + } + }.onFailure { + status = PackageInstaller.STATUS_FAILURE + message = it.message + } + } + return Pair(status, message) + } + + suspend fun uninstall(packageName: String): Pair { + var status = PackageInstaller.STATUS_FAILURE + var message: String? = null + withContext(Dispatchers.IO) { + runCatching { + if (ShizukuApi.isPermissionGranted) { + var result: Intent? = null + suspendCoroutine { cont -> + val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent -> + result = intent + cont.resume(Unit) + } + ShizukuApi.uninstallPackage(packageName, IntentSenderHelper.newIntentSender(adapter)) + } + result?.let { + status = it.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + message = it.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + } + } else { + val intent = Intent(Intent.ACTION_DELETE, Uri.parse("package:$packageName")) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + lspApp.startActivity(intent) + status = PackageInstaller.STATUS_PENDING_USER_ACTION + } + }.onFailure { + status = PackageInstaller.STATUS_FAILURE + message = it.message + } + } + return Pair(status, message) + } + + suspend fun getAppInfoFromApks(apks: List): Result> { + return withContext(Dispatchers.IO) { + runCatching { + var primary: ApplicationInfo? = null + val splits = mutableListOf() + val appInfos = apks.mapNotNull { uri -> + val src = DocumentFile.fromSingleUri(lspApp, uri) ?: return@mapNotNull null + val dst = lspApp.tmpApkDir.resolve(src.name!!) + lspApp.contentResolver.openInputStream(uri)?.use { input -> + dst.outputStream().use { output -> input.copyTo(output) } + } + val appInfo = lspApp.packageManager.getPackageArchiveInfo(dst.absolutePath, PackageManager.GET_META_DATA)?.applicationInfo + appInfo?.sourceDir = dst.absolutePath + if (appInfo == null) { + splits.add(dst.absolutePath) + return@mapNotNull null + } + if (primary == null) primary = appInfo + AppInfo(appInfo, lspApp.packageManager.getApplicationLabel(appInfo).toString()) + } + primary?.splitSourceDirs = splits.toTypedArray() + if (appInfos.isEmpty()) throw IOException("No apks") + appInfos + }.onFailure { + cleanTmpApkDir() + } + } + } + + fun getLaunchIntentForPackage(packageName: String): Intent? { + val pm = lspApp.packageManager + return pm.getLaunchIntentForPackage(packageName)?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + fun getSettingsIntent(packageName: String): Intent? { + val intent = Intent(SETTINGS_CATEGORY).setPackage(packageName).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val ris = lspApp.packageManager.queryIntentActivities(intent, 0) + return if (ris.isNotEmpty()) intent else getLaunchIntentForPackage(packageName) + } +} diff --git a/manager/src/main/java/nkbe/util/ShizukuApi.kt b/manager/src/main/java/nkbe/util/ShizukuApi.kt new file mode 100644 index 00000000..44882c9d --- /dev/null +++ b/manager/src/main/java/nkbe/util/ShizukuApi.kt @@ -0,0 +1,106 @@ +package nkbe.util + +import android.content.Intent +import android.content.IntentSender +import android.content.pm.* +import android.net.Uri +import android.os.Build +import android.os.IBinder +import android.os.IInterface +import android.os.Process +import android.os.SystemProperties +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import dev.rikka.tools.refine.Refine +import rikka.shizuku.Shizuku +import rikka.shizuku.ShizukuBinderWrapper +import rikka.shizuku.SystemServiceHelper +import java.io.File + +object ShizukuApi { + + private fun IBinder.wrap() = ShizukuBinderWrapper(this) + private fun IInterface.asShizukuBinder() = this.asBinder().wrap() + + private val iPackageManager: IPackageManager by lazy { + IPackageManager.Stub.asInterface(SystemServiceHelper.getSystemService("package").wrap()) + } + + private val iPackageInstaller: IPackageInstaller by lazy { + IPackageInstaller.Stub.asInterface(iPackageManager.packageInstaller.asShizukuBinder()) + } + + private val packageInstaller: PackageInstaller? by lazy { + val userId = Process.myUserHandle().hashCode() + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Refine.unsafeCast(PackageInstallerHidden(iPackageInstaller, "com.android.shell", null, userId)) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Refine.unsafeCast(PackageInstallerHidden(iPackageInstaller, "com.android.shell", userId)) + } else { + null + } + } catch (e: Throwable) { + null + } + } + + var isBinderAvailable = false + var isPermissionGranted by mutableStateOf(false) + + fun init() { + Shizuku.addBinderReceivedListenerSticky { + isBinderAvailable = true + isPermissionGranted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED + } + Shizuku.addBinderDeadListener { + isBinderAvailable = false + isPermissionGranted = false + } + } + + fun getInstalledApplications(): List { + val userId = Process.myUserHandle().hashCode() + val flags: Long = PackageManager.GET_META_DATA.toLong() + return iPackageManager.getInstalledApplications(flags, userId).list + } + + fun createPackageInstallerSession(params: PackageInstaller.SessionParams): PackageInstaller.Session? { + val installer = packageInstaller ?: return null + val sessionId = installer.createSession(params) + val iSession = IPackageInstallerSession.Stub.asInterface(iPackageInstaller.openSession(sessionId).asShizukuBinder()) + return Refine.unsafeCast(PackageInstallerHidden.SessionHidden(iSession)) + } + + fun isPackageInstalledWithoutPatch(packageName: String): Boolean { + val userId = Process.myUserHandle().hashCode() + val app = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + iPackageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA.toLong(), userId) + } else { + iPackageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA, userId) + } + return (app != null) && (app.metaData?.containsKey("npatch") != true) + } + + fun uninstallPackage(packageName: String, intentSender: IntentSender) { + packageInstaller?.uninstall(packageName, intentSender) + } + + fun performDexOptMode(packageName: String): Boolean { + return iPackageManager.performDexOptMode( + packageName, + SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false), + "verify", true, true, null + ) + } + + fun installApkNormal(context: android.content.Context, apkFile: File) { + val uri = androidx.core.content.FileProvider.getUriForFile(context, context.packageName + ".fileprovider", apkFile) + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(uri, "application/vnd.android.package-archive") + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } +} diff --git a/manager/src/main/java/org/lsposed/npatch/CrashTrap.kt b/manager/src/main/java/org/lsposed/npatch/CrashTrap.kt new file mode 100644 index 00000000..aabc0f32 --- /dev/null +++ b/manager/src/main/java/org/lsposed/npatch/CrashTrap.kt @@ -0,0 +1,46 @@ +package org.lsposed.npatch + +import android.content.Context +import android.os.Build +import java.io.File +import java.io.PrintWriter +import java.io.StringWriter + +object CrashTrap { + @JvmStatic + fun start(ctx: Context?) { + val context = ctx ?: return + val originalHandler = Thread.getDefaultUncaughtExceptionHandler() + + Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + try { + val sw = StringWriter() + throwable.printStackTrace(PrintWriter(sw)) + + val report = buildString { + appendLine("--- NPATCH CRASH REPORT ---") + appendLine("Device: ${Build.MODEL}") + appendLine("Android: ${Build.VERSION.RELEASE}") + appendLine("Thread: ${thread.name}") + appendLine("Reason: ${throwable.message}") + appendLine() + appendLine("--- STACK TRACE ---") + appendLine(sw.toString()) + appendLine() + appendLine("--- CAUSED BY ---") + var cause = throwable.cause + while (cause != null) { + appendLine("Caused by: ${cause.javaClass.name}: ${cause.message}") + cause.stackTrace.take(5).forEach { appendLine("\tat $it") } + cause = cause.cause + } + } + + val logDir = context.getExternalFilesDir(null) ?: context.filesDir + val logFile = File(logDir, "NPATCH_CRASH.txt") + logFile.writeText(report) + } catch (e: Exception) {} + originalHandler?.uncaughtException(thread, throwable) + } + } +} diff --git a/manager/src/main/java/org/lsposed/npatch/LSPApplication.kt b/manager/src/main/java/org/lsposed/npatch/LSPApplication.kt new file mode 100644 index 00000000..d2ec8d74 --- /dev/null +++ b/manager/src/main/java/org/lsposed/npatch/LSPApplication.kt @@ -0,0 +1,73 @@ +package org.lsposed.npatch + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.os.Process +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.lsposed.hiddenapibypass.HiddenApiBypass +import org.lsposed.npatch.manager.AppBroadcastReceiver +import nkbe.util.NPackageManager +import nkbe.util.ShizukuApi +import java.io.File + +lateinit var lspApp: LSPApplication + +class LSPApplication : Application() { + + lateinit var prefs: SharedPreferences + lateinit var tmpApkDir: File + + var targetApkFiles: ArrayList? = null + val globalScope = CoroutineScope(Dispatchers.Default) + + + override fun onCreate() { + super.onCreate() + // verifySignature() + org.lsposed.npatch.CrashTrap.start(this) + + try { + } catch (e: UnsatisfiedLinkError) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + } + //HiddenApiBypass.addHiddenApiExemptions("") + lspApp = this + filesDir.mkdir() + tmpApkDir = cacheDir.resolve("apk").also { it.mkdir() } + prefs = lspApp.getSharedPreferences("settings", Context.MODE_PRIVATE) + ShizukuApi.init() + AppBroadcastReceiver.register(this) + globalScope.launch { NPackageManager.fetchAppList() } + } + + private fun verifySignature() { + try { + val flags = PackageManager.GET_SIGNING_CERTIFICATES + val packageInfo = packageManager.getPackageInfo(packageName, flags) + val signingInfo = packageInfo.signingInfo + val signatures = signingInfo?.apkContentsSigners + + if (signatures != null && signatures.isNotEmpty()) { + val currentHash = signatures[0].hashCode() + val targetHash = 0x0293FA43 + if (currentHash != targetHash) { + killApp() + } + } else { + killApp() + } + } catch (e: Exception) { + killApp() + } + } + + private fun killApp() { + Process.killProcess(Process.myPid()) + } +} diff --git a/manager/src/main/java/org/lsposed/npatch/Patcher.kt b/manager/src/main/java/org/lsposed/npatch/Patcher.kt new file mode 100644 index 00000000..7c298e91 --- /dev/null +++ b/manager/src/main/java/org/lsposed/npatch/Patcher.kt @@ -0,0 +1,127 @@ +package org.lsposed.npatch + +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.lsposed.npatch.config.Configs +import org.lsposed.npatch.config.MyKeyStore +import org.lsposed.npatch.share.Constants +import org.lsposed.npatch.share.PatchConfig +import org.lsposed.patch.NPatch +import org.lsposed.patch.util.Logger +import java.io.File +import java.io.IOException + +object Patcher { + + private const val APK_MIME_TYPE = "application/vnd.android.package-archive" + + class Options( + val newPackageName: String, + private val injectDex: Boolean, + private val config: PatchConfig, + private val apkPaths: List, + private val embeddedModules: List? + ) { + fun toStringArray(): Array { + return buildList { + add("-o"); add(lspApp.tmpApkDir.absolutePath) + add("-p"); add(config.newPackage) + if (config.debuggable) add("-d") + add("-l"); add(config.sigBypassLevel.toString()) + if (config.useManager) add("--manager") + if (config.overrideVersionCode) add("-r") + if (Configs.detailPatchLogs) add("-v") + embeddedModules?.forEach { + add("-m"); add(it) + } + if (config.injectProvider) add("--provider") + if(injectDex) add("--injectdex") + if (config.useMicroG) add("--useMicroG") + if (!MyKeyStore.useDefault) { + addAll(arrayOf("-k", MyKeyStore.file.path, Configs.keyStorePassword, Configs.keyStoreAlias, Configs.keyStoreAliasPassword)) + } + addAll(apkPaths) + }.toTypedArray() + } + } + + suspend fun patch(logger: Logger, options: Options) { + withContext(Dispatchers.IO) { + cleanupPatchedArtifacts() + NPatch(logger, *options.toStringArray()).doCommandLine() + + val uri = Configs.storageDirectory?.toUri() + ?: throw IOException("Uri is null") + val root = DocumentFile.fromTreeUri(lspApp, uri) + ?: throw IOException("DocumentFile is null") + deletePatchedApks(root) + lspApp.targetApkFiles?.clear() + val apkFileList = collectPatchedApks() + if (apkFileList.isEmpty()) { + throw IOException("No patched APK files found in ${lspApp.tmpApkDir.absolutePath}") + } + apkFileList.forEach { cachedApkFile -> + val existingFile = root.findFile(cachedApkFile.name) + if (existingFile?.delete() == false) { + throw IOException("Unable to replace output file: ${cachedApkFile.name}") + } + val finalFile = root.createFile(APK_MIME_TYPE, cachedApkFile.name) + ?: throw IOException("無法建立輸出檔案: ${cachedApkFile.name}") + lspApp.contentResolver.openOutputStream(finalFile.uri, "wt")?.use { output -> + cachedApkFile.inputStream().use { input -> + input.copyTo(output) + } + } ?: throw IOException("Unable to open an output stream: ${finalFile.uri}") + } + lspApp.targetApkFiles = apkFileList + logger.i("Patched files are saved to ${root.uri.lastPathSegment}") + } + } + + private fun cleanupPatchedArtifacts() { + deletePatchedApks(lspApp.tmpApkDir) + lspApp.externalCacheDir?.let { deletePatchedApks(it) } + } + + private fun collectPatchedApks(): ArrayList { + val externalCacheDir = lspApp.externalCacheDir + ?: throw IOException("External cache directory is unavailable") + return lspApp.tmpApkDir.walkTopDown() + .filter { it.isFile && it.name.endsWith(Constants.PATCH_FILE_SUFFIX) } + .sortedBy { it.name } + .mapTo(arrayListOf()) { tempApkFile -> + val cachedApkFile = externalCacheDir.resolve(tempApkFile.name) + if (cachedApkFile.exists() && !cachedApkFile.delete()) { + throw IOException("Unable to clear cached APK: ${cachedApkFile.absolutePath}") + } + if (!tempApkFile.renameTo(cachedApkFile)) { + tempApkFile.copyTo(cachedApkFile, overwrite = true) + if (!tempApkFile.delete()) { + throw IOException("Unable to remove temp APK: ${tempApkFile.absolutePath}") + } + } + cachedApkFile + } + } + + private fun deletePatchedApks(directory: File) { + if (!directory.exists()) return + directory.walkBottomUp() + .filter { it.isFile && it.name.endsWith(Constants.PATCH_FILE_SUFFIX) } + .forEach { + if (!it.delete()) { + throw IOException("Unable to delete stale APK: ${it.absolutePath}") + } + } + } + + private fun deletePatchedApks(root: DocumentFile) { + root.listFiles().forEach { + if (it.name?.endsWith(Constants.PATCH_FILE_SUFFIX) == true && !it.delete()) { + throw IOException("Unable to delete stale output file: ${it.name}") + } + } + } +} diff --git a/manager/src/main/java/org/lsposed/npatch/config/ConfigManager.kt b/manager/src/main/java/org/lsposed/npatch/config/ConfigManager.kt new file mode 100644 index 00000000..0b0e3f40 --- /dev/null +++ b/manager/src/main/java/org/lsposed/npatch/config/ConfigManager.kt @@ -0,0 +1,91 @@ +package org.lsposed.npatch.config + +import android.content.pm.PackageManager +import android.util.Log +import androidx.room.Room +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.withContext +import org.lsposed.npatch.database.LSPDatabase +import org.lsposed.npatch.database.entity.Module +import org.lsposed.npatch.database.entity.Scope +import org.lsposed.npatch.lspApp +import org.lsposed.npatch.util.ModuleLoader +import java.io.File + +object ConfigManager { + + private const val TAG = "ConfigManager" + + @OptIn(ExperimentalCoroutinesApi::class) + private val dispatcher = Dispatchers.Default.limitedParallelism(1) + + private val db: LSPDatabase by lazy { + Room.databaseBuilder( + lspApp, LSPDatabase::class.java, "modules_config.db" + ).build() + } + + + private val moduleDao get() = db.moduleDao() + private val scopeDao get() = db.scopeDao() + + private val loadedModules = mutableMapOf() + + suspend fun updateModules(newModules: Map) = + withContext(dispatcher) { + for (module in moduleDao.getAll()) { + val apkPath = newModules[module.pkgName] + if (apkPath == null) { + moduleDao.delete(module) + loadedModules.remove(module) + } else if (module.apkPath != apkPath) { + module.apkPath = apkPath + loadedModules.remove(module) + } + } + for ((pkgName, apkPath) in newModules) { + moduleDao.insert(Module(pkgName, apkPath)) + } + } + + suspend fun activateModule(pkgName: String, module: Module) = + withContext(dispatcher) { + scopeDao.insert(Scope(appPkgName = pkgName, modulePkgName = module.pkgName)) + } + + suspend fun deactivateModule(pkgName: String, module: Module) = + withContext(dispatcher) { + scopeDao.delete(Scope(appPkgName = pkgName, modulePkgName = module.pkgName)) + } + + suspend fun getModulesForApp(pkgName: String): List = + withContext(dispatcher) { + return@withContext scopeDao.getModulesForApp(pkgName) + } + + suspend fun getModuleFilesForApp(pkgName: String): List = + withContext(dispatcher) { + val modules = scopeDao.getModulesForApp(pkgName) + return@withContext modules.mapNotNull { + if (!File(it.apkPath).exists()) { + loadedModules.remove(it) + try { + it.apkPath = lspApp.packageManager.getApplicationInfo(it.pkgName, 0).sourceDir + } catch (e: PackageManager.NameNotFoundException) { + moduleDao.delete(moduleDao.getModule(it.pkgName)) + Log.w(TAG, "Module may be uninstalled: ${it.pkgName}") + return@mapNotNull null + } + Log.i(TAG, "Module apk path updated: ${it.pkgName}") + } + loadedModules.getOrPut(it) { + org.lsposed.lspd.models.Module().apply { + packageName = it.pkgName + apkPath = it.apkPath + file = ModuleLoader.loadModule(it.apkPath) + } + } + } + } +} diff --git a/manager/src/main/java/org/lsposed/npatch/config/Configs.kt b/manager/src/main/java/org/lsposed/npatch/config/Configs.kt new file mode 100644 index 00000000..9a376110 --- /dev/null +++ b/manager/src/main/java/org/lsposed/npatch/config/Configs.kt @@ -0,0 +1,35 @@ +package org.lsposed.npatch.config + +import org.lsposed.npatch.lspApp +import org.lsposed.npatch.ui.util.delegateStateOf +import org.lsposed.npatch.ui.util.getValue +import org.lsposed.npatch.ui.util.setValue + +object Configs { + + private const val PREFS_KEYSTORE_PASSWORD = "keystore_password" + private const val PREFS_KEYSTORE_ALIAS = "keystore_alias" + private const val PREFS_KEYSTORE_ALIAS_PASSWORD = "keystore_alias_password" + private const val PREFS_STORAGE_DIRECTORY = "storage_directory" + private const val PREFS_DETAIL_PATCH_LOGS = "detail_patch_logs" + + var keyStorePassword by delegateStateOf(lspApp.prefs.getString(PREFS_KEYSTORE_PASSWORD, "123456")!!) { + lspApp.prefs.edit().putString(PREFS_KEYSTORE_PASSWORD, it).apply() + } + + var keyStoreAlias by delegateStateOf(lspApp.prefs.getString(PREFS_KEYSTORE_ALIAS, "key0")!!) { + lspApp.prefs.edit().putString(PREFS_KEYSTORE_ALIAS, it).apply() + } + + var keyStoreAliasPassword by delegateStateOf(lspApp.prefs.getString(PREFS_KEYSTORE_ALIAS_PASSWORD, "123456")!!) { + lspApp.prefs.edit().putString(PREFS_KEYSTORE_ALIAS_PASSWORD, it).apply() + } + + var storageDirectory by delegateStateOf(lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)) { + lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, it).apply() + } + + var detailPatchLogs by delegateStateOf(lspApp.prefs.getBoolean(PREFS_DETAIL_PATCH_LOGS, true)) { + lspApp.prefs.edit().putBoolean(PREFS_DETAIL_PATCH_LOGS, it).apply() + } +} diff --git a/manager/src/main/res/mipmap-anydpi-v26/ic_launcher_round.png b/manager/src/main/res/mipmap-anydpi-v26/ic_launcher_round.png new file mode 100644 index 00000000..e54b99ab Binary files /dev/null and b/manager/src/main/res/mipmap-anydpi-v26/ic_launcher_round.png differ diff --git a/manager/src/main/res/mipmap-xxxhdpi-v4/ic_launcher.png b/manager/src/main/res/mipmap-xxxhdpi-v4/ic_launcher.png new file mode 100644 index 00000000..e54b99ab Binary files /dev/null and b/manager/src/main/res/mipmap-xxxhdpi-v4/ic_launcher.png differ diff --git a/meta-loader/build.gradle.kts b/meta-loader/build.gradle.kts index d6cf466c..d3f344ba 100644 --- a/meta-loader/build.gradle.kts +++ b/meta-loader/build.gradle.kts @@ -6,8 +6,13 @@ plugins { android { defaultConfig { - multiDexEnabled = false + multiDexEnabled = true + sourceSets { + named("main") { + java.srcDir("${rootProject.projectDir}/src") + } } +} buildTypes { release { @@ -47,5 +52,5 @@ androidComponents.onVariants { variant -> dependencies { compileOnly("vector:stubs") implementation(projects.share.java) - implementation(libs.hiddenapibypass) + implementation(libs.hiddenapibypass) } diff --git a/meta-loader/proguard-rules.pro b/meta-loader/proguard-rules.pro index cb7f0b9a..214b3929 100644 --- a/meta-loader/proguard-rules.pro +++ b/meta-loader/proguard-rules.pro @@ -8,6 +8,10 @@ -keep interface * extends androidx.room.Dao { ; } +-keep class android.** { *; +} +-keep class com.android.** { *; +} -dontwarn androidx.annotation.NonNull -dontwarn androidx.annotation.Nullable diff --git a/meta-loader/src/main/java/org/lsposed/npatch/metaloader/LSPAppComponentFactoryStub.java b/meta-loader/src/main/java/org/lsposed/npatch/metaloader/LSPAppComponentFactoryStub.java index 5c2c3c5b..4fdd3b0d 100644 --- a/meta-loader/src/main/java/org/lsposed/npatch/metaloader/LSPAppComponentFactoryStub.java +++ b/meta-loader/src/main/java/org/lsposed/npatch/metaloader/LSPAppComponentFactoryStub.java @@ -2,7 +2,7 @@ import android.annotation.SuppressLint; import android.app.ActivityThread; -import android.app.AppComponentFactory; +import android.app.AppComponentFactoryStub; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.os.Build; @@ -29,7 +29,7 @@ import java.util.zip.ZipFile; @SuppressLint("UnsafeDynamicallyLoadedCode") -public class LSPAppComponentFactoryStub extends AppComponentFactory { +public class LSPAppComponentFactoryStub extends AppComponentFactoryStub { private static final String TAG = "NPatch-MetaLoader"; private static final Map archToLib = new HashMap(4); @@ -37,13 +37,13 @@ public class LSPAppComponentFactoryStub extends AppComponentFactory { public static byte[] dex; static { - final boolean appZygote = ActivityThread.currentActivityThread() == null; - if (appZygote) { - Log.i(TAG, "Skip loading libnpatch.so for appZygote"); - } else { - bootstrap(); - } + final boolean appZygote = ActivityThread.currentActivityThread() == null; + if (appZygote) { + Log.i(TAG, "Skip loading libnpatch.so for appZygote"); + } else { + bootstrap(); } +} private static void bootstrap() { try { @@ -133,23 +133,4 @@ private static void transfer(InputStream is, OutputStream os) throws IOException os.write(buffer, 0, n); } } - - private static File createTempSoFile() throws IOException { - String packageName = null; - try { - var currentPackageName = ActivityThread.class.getDeclaredMethod("currentPackageName"); - currentPackageName.setAccessible(true); - packageName = (String) currentPackageName.invoke(null); - } catch (Throwable ignored) { - } - if (packageName == null || packageName.isEmpty()) { - throw new IOException("Unable to resolve current package name"); - } - - File baseDir = new File("/data/user/0/" + packageName + "/cache"); - if (!baseDir.exists() && !baseDir.mkdirs()) { - throw new IOException("Unable to create cache directory: " + baseDir); - } - return File.createTempFile("libnpatch-", ".so", baseDir); } -} diff --git a/patch-loader/build.gradle.kts b/patch-loader/build.gradle.kts index 063b5d11..51da9bdc 100644 --- a/patch-loader/build.gradle.kts +++ b/patch-loader/build.gradle.kts @@ -6,16 +6,7 @@ plugins { android { defaultConfig { - multiDexEnabled = false - - externalNativeBuild { - cmake { - arguments += "-DCORE_ROOT=${File(rootDir.absolutePath, "core/native") }" - arguments += "-DEXTERNAL_ROOT=${File(rootDir.absolutePath, "core/external") }" - arguments += "-DVERSION_CODE=${rootProject.extra["verCode"]}" - arguments += "-DVERSION_NAME=${rootProject.extra["verName"]}" - } - } + multiDexEnabled = true } buildFeatures { @@ -81,7 +72,7 @@ dependencies { implementation("vector:core") implementation("vector:bridge") implementation("vector:daemon-service") - implementation("vector:legacy") + //implementation("vector:legacy") implementation(projects.share.android) implementation(projects.share.java) diff --git a/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPLoader.java b/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPLoader.java index 88ba2acd..4da3c269 100644 --- a/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPLoader.java +++ b/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPLoader.java @@ -28,15 +28,13 @@ public static void initModules(LoadedApk loadedApk) { private static void setPackageNameForResDir(String packageName, String resDir) { try { - // Use reflection to avoid direct type reference to android.content.res.XResources - // which fails class resolution on Android 16+ due to strict boot classloader - // namespace delegation for the android.content.res.* package. - ClassLoader cl = LSPLoader.class.getClassLoader(); - Class xResourcesClass = cl.loadClass("android.content.res.XResources"); + Class xResourcesClass = Class.forName( + "android.content.res.XResources",false, + Thread.currentThread().getContextClassLoader() ); Method setMethod = xResourcesClass.getMethod("setPackageNameForResDir", String.class, String.class); setMethod.invoke(null, packageName, resDir); } catch (Throwable e) { Log.w(TAG, "XResources.setPackageNameForResDir not available, skipping resource dir setup", e); } } -} \ No newline at end of file +} diff --git a/patch-loader/src/main/java/org/lsposed/npatch/service/NeoLocalApplicationService.java b/patch-loader/src/main/java/org/lsposed/npatch/service/NeoLocalApplicationService.java index fef5089e..97747ecb 100644 --- a/patch-loader/src/main/java/org/lsposed/npatch/service/NeoLocalApplicationService.java +++ b/patch-loader/src/main/java/org/lsposed/npatch/service/NeoLocalApplicationService.java @@ -49,13 +49,20 @@ private void loadModulesFromCache(Context context) { Log.i(TAG, "NeoLocal: Loading from cache: " + jsonStr); for (int i = 0; i < jsonArray.length(); i++) { - JSONObject obj = jsonArray.getJSONObject(i); - String packageName = obj.optString("packageName"); - String path = obj.optString("path"); + Object item = jsonArray.get(i); + String packageName = ""; + String path = ""; + + if (item instanceof JSONObject) { + packageName = ((JSONObject) item).optString("packageName"); + path = ((JSONObject) item).optString("path"); + } else { + packageName = item.toString(); + } if (path != null && !path.isEmpty() && new File(path).exists()) { loadModuleByPath(packageName, path); - } else if (packageName != null) { + } else if (packageName != null && !packageName.isEmpty()) { loadSingleModule(pm, packageName); } } @@ -87,14 +94,18 @@ private void loadModulesFromProvider(Context context) { try (Cursor cursor = context.getContentResolver().query(queryUri, null, null, null, null)) { if (cursor == null) { - Log.w(TAG, "NeoLocal: Cannot reach Manager Provider."); + Log.w(TAG, "NeoLocal: Provider query returned null."); + return; + } + int colIndex = cursor.getColumnIndex("packageName"); + if (colIndex == -1) { + Log.e(TAG, "NeoLocal: Column 'packageName' not found in provider."); return; } - while (cursor.moveToNext()) { - int colIndex = cursor.getColumnIndex("packageName"); - if (colIndex != -1) { - loadSingleModule(pm, cursor.getString(colIndex)); + String pkg = cursor.getString(colIndex); + if (pkg != null && !pkg.isEmpty()) { + loadSingleModule(pm, pkg); } } } catch (Exception e) { @@ -131,8 +142,10 @@ public List getModulesList() throws RemoteException { @Override public String getPrefsPath(String packageName) throws RemoteException { return "/data/data/" + packageName + "/shared_prefs/"; } + @Override public ParcelFileDescriptor requestInjectedManagerBinder(List binder) throws RemoteException { return null; } + @Override public IBinder asBinder() { return this; @@ -142,4 +155,4 @@ public IBinder asBinder() { public boolean isLogMuted() throws RemoteException { return false; } -} + } diff --git a/patch-loader/src/main/jni/CMakeLists.txt b/patch-loader/src/main/jni/CMakeLists.txt index 5aa924db..7125fbd4 100644 --- a/patch-loader/src/main/jni/CMakeLists.txt +++ b/patch-loader/src/main/jni/CMakeLists.txt @@ -37,4 +37,4 @@ if (DEFINED DEBUG_SYMBOLS_PATH) COMMAND ${CMAKE_STRIP} --strip-all $ COMMAND ${CMAKE_OBJCOPY} --add-gnu-debuglink ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug $) -endif() \ No newline at end of file +endif() diff --git a/patch/src/main/java/org/lsposed/patch/NPatch.java b/patch/src/main/java/org/lsposed/patch/NPatch.java index 3dd5f417..ff3ea406 100644 --- a/patch/src/main/java/org/lsposed/patch/NPatch.java +++ b/patch/src/main/java/org/lsposed/patch/NPatch.java @@ -224,7 +224,7 @@ public void patch(File srcApkFile, File outputFile) throws PatchError, IOExcepti } var entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(keystoreArgs.get(2), new KeyStore.PasswordProtection(keystoreArgs.get(3).toCharArray())); new SigningExtension(SigningOptions.builder() - .setMinSdkVersion(27) + .setMinSdkVersion(24) .setV2SigningEnabled(true) .setCertificates((X509Certificate[]) entry.getCertificateChain()) .setKey(entry.getPrivateKey()) @@ -324,8 +324,8 @@ public void patch(File srcApkFile, File outputFile) throws PatchError, IOExcepti } } catch (Throwable e) { throw new PatchError("Error when adding dex", e); - } - + } + if (isInjectProvider){ try (var is = getClass().getClassLoader().getResourceAsStream("assets/mtprovider.dex")) { dstZFile.add("assets/npatch/mtprovider.dex", is); @@ -496,4 +496,4 @@ private byte[] modifyManifestFile(InputStream is, String metadata, int minSdkVer if (is != null) is.close(); } } -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 79985553..6ed17a7e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,9 +49,9 @@ includeBuild("core") { dependencySubstitution { substitute(module("vector:axml")).using(project(":external:axml")) substitute(module("vector:bridge")).using(project(":hiddenapi:bridge")) - substitute(module("vector:legacy")).using(project(":legacy")) + //substitute(module("vector:legacy")).using(project(":legacy")) substitute(module("vector:core")).using(project(":xposed")) substitute(module("vector:daemon-service")).using(project(":services:daemon-service")) substitute(module("vector:stubs")).using(project(":hiddenapi:stubs")) } -} \ No newline at end of file +} diff --git a/src/android/app/AppComponentFactoryBackport.java b/src/android/app/AppComponentFactoryBackport.java new file mode 100644 index 00000000..ad168a84 --- /dev/null +++ b/src/android/app/AppComponentFactoryBackport.java @@ -0,0 +1,120 @@ +package android.app; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ProviderInfo; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.net.Uri; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class AppComponentFactoryBackport { + + public static class AutoInit extends ContentProvider { + @Override + public void attachInfo(Context context, ProviderInfo info) { + super.attachInfo(context, info); + init(context); + } + + private void init(Context context) { + try { + String fName = null; + try (XmlResourceParser p = context.getPackageManager().getXml(context.getPackageName(), 0, null)) { + int t; + while ((t = p.next()) != 1) { + if (t == 2 && "application".equals(p.getName())) { + for (int i = 0; i < p.getAttributeCount(); i++) { + if ("appComponentFactory".equals(p.getAttributeName(i))) { + fName = p.getAttributeValue(i); + break; + } + } + break; + } + } + } + + if (fName == null) return; + + ClassLoader cl = context.getClassLoader(); + AppComponentFactory factory = (AppComponentFactory) cl.loadClass(fName).newInstance(); + + Class atc = Class.forName("android.app.ActivityThread"); + Object at = atc.getDeclaredMethod("currentActivityThread").invoke(null); + + Field mBoundAppField = atc.getDeclaredField("mBoundApplication"); + mBoundAppField.setAccessible(true); + Object boundApp = mBoundAppField.get(at); + + Field infoField = boundApp.getClass().getDeclaredField("info"); + infoField.setAccessible(true); + Object loadedApk = infoField.get(boundApp); + + try { + Field fFactory = loadedApk.getClass().getDeclaredField("mAppComponentFactory"); + fFactory.setAccessible(true); + fFactory.set(loadedApk, factory); + } catch (Throwable ignored) {} + + Field fInst = atc.getDeclaredField("mInstrumentation"); + fInst.setAccessible(true); + Instrumentation base = (Instrumentation) fInst.get(at); + + if (!(base instanceof ProxyInst)) { + fInst.set(at, new ProxyInst(base, factory)); + } + } catch (Throwable ignored) {} + } + + @Override public boolean onCreate() { return true; } + @Override public Cursor query(Uri u, String[] p, String s, String[] a, String o) { return null; } + @Override public String getType(Uri u) { return null; } + @Override public Uri insert(Uri u, ContentValues v) { return null; } + @Override public int delete(Uri u, String s, String[] a) { return 0; } + @Override public int update(Uri u, ContentValues v, String s, String[] a) { return 0; } + } + + private static class ProxyInst extends Instrumentation { + private final Instrumentation b; + private final AppComponentFactory f; + + ProxyInst(Instrumentation base, AppComponentFactory factory) { + this.b = base; + this.f = factory; + } + + @Override + public Application newApplication(ClassLoader cl, String className, Context ctx) throws InstantiationException, IllegalAccessException, ClassNotFoundException { + try { + Application app = f.instantiateApplication(cl, className); + Method m = Application.class.getDeclaredMethod("attach", Context.class); + m.setAccessible(true); + m.invoke(app, ctx); + return app; + } catch (Throwable e) { + return b.newApplication(cl, className, ctx); + } + } + + @Override + public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { + try { + return f.instantiateActivity(cl, className, intent); + } catch (Throwable e) { + return b.newActivity(cl, className, intent); + } + } + + @Override public void callApplicationOnCreate(Application a) { b.callApplicationOnCreate(a); } + @Override public void callActivityOnCreate(Activity a, android.os.Bundle i) { b.callActivityOnCreate(a, i); } + @Override public void callActivityOnDestroy(Activity a) { b.callActivityOnDestroy(a); } + @Override public void callActivityOnPause(Activity a) { b.callActivityOnPause(a); } + @Override public void callActivityOnResume(Activity a) { b.callActivityOnResume(a); } + @Override public void callActivityOnStart(Activity a) { b.callActivityOnStart(a); } + @Override public void callActivityOnStop(Activity a) { b.callActivityOnStop(a); } + } + } diff --git a/src/android/app/AppComponentFactoryStub.java b/src/android/app/AppComponentFactoryStub.java new file mode 100644 index 00000000..8e8c5846 --- /dev/null +++ b/src/android/app/AppComponentFactoryStub.java @@ -0,0 +1,27 @@ +package android.app; + +import android.content.BroadcastReceiver; +import android.content.ContentProvider; +import android.content.Intent; + +public class AppComponentFactoryStub { + public Application instantiateApplication(ClassLoader cl, String className) throws Exception { + return (Application) cl.loadClass(className).newInstance(); + } + + public Activity instantiateActivity(ClassLoader cl, String className, Intent intent) throws Exception { + return (Activity) cl.loadClass(className).newInstance(); + } + + public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent) throws Exception { + return (BroadcastReceiver) cl.loadClass(className).newInstance(); + } + + public Service instantiateService(ClassLoader cl, String className, Intent intent) throws Exception { + return (Service) cl.loadClass(className).newInstance(); + } + + public ContentProvider instantiateProvider(ClassLoader cl, String className) throws Exception { + return (ContentProvider) cl.loadClass(className).newInstance(); + } + } diff --git a/src/android/os/SharedMemory.java b/src/android/os/SharedMemory.java new file mode 100644 index 00000000..c036dc0a --- /dev/null +++ b/src/android/os/SharedMemory.java @@ -0,0 +1,103 @@ +package android.os; + +import java.io.Closeable; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +public final class SharedMemory implements Parcelable, Closeable { + + private final FileDescriptor mFileDescriptor; + private final int mSize; + private final MemoryFile mMemoryFile; + + private SharedMemory(FileDescriptor fd, int size, MemoryFile memoryFile) { + this.mFileDescriptor = fd; + this.mSize = size; + this.mMemoryFile = memoryFile; + } + + public static SharedMemory create(String name, int size) throws Exception { + if (size <= 0) throw new IllegalArgumentException(); + MemoryFile memoryFile = new MemoryFile(name, size); + Method getFdMethod = MemoryFile.class.getDeclaredMethod("getFileDescriptor"); + getFdMethod.setAccessible(true); + FileDescriptor fd = (FileDescriptor) getFdMethod.invoke(memoryFile); + return new SharedMemory(fd, size, memoryFile); + } + + public static ClassLoader InMemoryDexClassLoader(ByteBuffer[] buffers, ClassLoader parent) { + try { + int totalSize = 0; + for (ByteBuffer buf : buffers) totalSize += buf.remaining(); + byte[] dexBytes = new byte[totalSize]; + int offset = 0; + for (ByteBuffer buf : buffers) { + int len = buf.remaining(); + buf.get(dexBytes, offset, len); + offset += len; + } + + Class dexFileClass = Class.forName("dalvik.system.DexFile"); + Method openDexFileMethod = dexFileClass.getDeclaredMethod("openDexFile", byte[].class); + openDexFileMethod.setAccessible(true); + Object cookie = openDexFileMethod.invoke(null, dexBytes); + + dalvik.system.DexClassLoader dummyLoader = new dalvik.system.DexClassLoader("", null, null, parent); + Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList"); + pathListField.setAccessible(true); + Object pathList = pathListField.get(dummyLoader); + + Field dexElementsField = pathList.getClass().getDeclaredField("dexElements"); + dexElementsField.setAccessible(true); + Object[] dexElements = (Object[]) dexElementsField.get(pathList); + + Field dexFileField = dexElements[0].getClass().getDeclaredField("dexFile"); + dexFileField.setAccessible(true); + Object dexFileObj = dexFileField.get(dexElements[0]); + + Field mCookieField = dexFileClass.getDeclaredField("mCookie"); + mCookieField.setAccessible(true); + mCookieField.set(dexFileObj, cookie); + + return dummyLoader; + } catch (Throwable e) { + return parent; + } + } + + public ByteBuffer mapReadOnly() throws Exception { + if (mFileDescriptor == null || !mFileDescriptor.valid()) throw new IllegalStateException(); + return new FileInputStream(mFileDescriptor).getChannel().map(FileChannel.MapMode.READ_ONLY, 0, mSize); + } + + public void setProtect(int prot) { + try { + Class libcore = Class.forName("libcore.io.Libcore"); + Field osField = libcore.getField("os"); + Object os = osField.get(null); + Method mprotect = os.getClass().getMethod("mprotect", long.class, long.class, int.class); + + FileInputStream fis = new FileInputStream(mFileDescriptor); + ByteBuffer buffer = fis.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, mSize); + + Field addressField = java.nio.Buffer.class.getDeclaredField("address"); + addressField.setAccessible(true); + long address = addressField.getLong(buffer); + mprotect.invoke(os, address, (long) mSize, prot); + } catch (Throwable ignored) {} + } + + @Override public void close() { if (mMemoryFile != null) mMemoryFile.close(); } + public int getSize() { return mSize; } + public FileDescriptor getFileDescriptor() { return mFileDescriptor; } + @Override public int describeContents() { return 1; } + @Override public void writeToParcel(Parcel dest, int flags) {} + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override public SharedMemory createFromParcel(Parcel source) { return null; } + @Override public SharedMemory[] newArray(int size) { return new SharedMemory[size]; } + }; +}