diff --git a/.github/workflows/PR_check.yml b/.github/workflows/PR_check.yml index e30ee90a..35c76043 100644 --- a/.github/workflows/PR_check.yml +++ b/.github/workflows/PR_check.yml @@ -46,4 +46,4 @@ jobs: id: buildDebug run: | ./gradlew :app:assembleDebug - echo "::set-output name=debugName::`ls app/build/outputs/apk/debug/V*-debug.apk | awk -F '(/|.apk)' '{print $6}'`" + echo "::set-output name=debugName::`ls app/build/apk/debug/HMA*-debug.apk | awk -F '(/|.apk)' '{print $6}'`" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ca34b64a..8307aa69 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,27 +57,27 @@ jobs: id: buildRelease run: | ./gradlew :app:assembleRelease - echo "::set-output name=releaseName::`ls app/build/outputs/apk/release/V*-release.apk | awk -F '(/|.apk)' '{print $6}'`" + echo "::set-output name=releaseName::`ls app/build/apk/release/HMA*-release.apk | awk -F '(/|.apk)' '{print $6}'`" - name: Build debug id: buildDebug run: | ./gradlew :app:assembleDebug - echo "::set-output name=debugName::`ls app/build/outputs/apk/debug/V*-debug.apk | awk -F '(/|.apk)' '{print $6}'`" + echo "::set-output name=debugName::`ls app/build/apk/debug/HMA*-debug.apk | awk -F '(/|.apk)' '{print $6}'`" - name: Upload release if: success() uses: actions/upload-artifact@v2 with: name: ${{ steps.buildRelease.outputs.releaseName }} - path: "app/build/outputs/apk/release/*.apk" + path: "app/build/apk/release/*.apk" - name: Upload debug if: success() uses: actions/upload-artifact@v2 with: name: ${{ steps.buildDebug.outputs.debugName }} - path: "app/build/outputs/apk/debug/*.apk" + path: "app/build/apk/debug/*.apk" - name: Upload mappings uses: actions/upload-artifact@v2 @@ -93,8 +93,8 @@ jobs: COMMIT_URL: ${{ github.event.head_commit.url }} COMMIT_MESSAGE: ${{ github.event.head_commit.message }} run: | - OUTPUT="app/build/outputs/apk/" - export release=$(find $OUTPUT -name "V*-release.apk") - export debug=$(find $OUTPUT -name "V*-debug.apk") + OUTPUT="app/build/apk/" + export release=$(find $OUTPUT -name "HMA*-release.apk") + export debug=$(find $OUTPUT -name "HMA*-debug.apk") ESCAPED=`python3 -c 'import json,os,urllib.parse; msg = json.dumps(os.environ["COMMIT_MESSAGE"]); print(urllib.parse.quote(msg if len(msg) <= 1024 else json.dumps(os.environ["COMMIT_URL"])))'` curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22:%22document%22,%20%22media%22:%22attach://release%22%7D,%7B%22type%22:%22document%22,%20%22media%22:%22attach://debug%22,%22caption%22:${ESCAPED}%7D%5D" -F release="@$release" -F debug="@$debug" diff --git a/README.md b/README.md index b5a4cea3..1244b8bf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Hide My Applist -## This project is suspended 此项目已停更 - [![Stars](https://img.shields.io/github/stars/Dr-TSNG/Hide-My-Applist?label=Stars)](https://github.com/Dr-TSNG) [![Release](https://img.shields.io/github/v/release/Dr-TSNG/Hide-My-Applist?label=Release)](https://github.com/Dr-TSNG/Hide-My-Applist/releases/latest) [![Download](https://img.shields.io/github/downloads/Dr-TSNG/Hide-My-Applist/total)](https://github.com/Dr-TSNG/Hide-My-Applist/releases/latest) @@ -27,18 +25,8 @@ This module can work as an Xposed module to hide apps or reject app list request 该模块提供了一些检测方式用于测试您是否成功地隐藏了某些特定的包名,如 Magisk/Edxposed Manager;同时可作为 Xposed 模块用于隐藏应用列表或特定应用,保护隐私。 ## Document -### Maps scan rules -Maps refers to /proc/self/maps (See [Linux manpage](https://man7.org/linux/man-pages/man5/proc.5.html) for more information). -When something such as an Xposed module or a Zygisk module injects into target app, it will show its path on /proc/\/maps. Though LSPosed and Riru did some work to make module maps info anonymous, if a module dlopen a native library by itself, the loaded so path will still be written on maps (Such module like QNotified). - -How to use it: paths that contains configured strings will be filtered on /proc/self/maps -Notice that under **MOST** circumstances you do not need to switch on this interception nor need to add any rule. ### Custom query params This refers to the string params of methods of [PackageManagerService](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java) How to use it: pms methods whose string params contain configured strings will be intercepted Notice that under **MOST** circumstances you do not need to switch on this interception nor need to add any rule. -### How did HMA Magisk module work? -HMA install inline hooks for syscalls and replace the path to dummy to make the app think there "isn't" suspicious files or directories. - -However, syscall hook is very **unstable**, and can be detected by some methods. So do not switch on *file detection / maps scan* interceptions if not needed. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ef4dc562..b75d5ea5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,152 +1,88 @@ -import com.android.build.api.component.analytics.AnalyticsEnabledApplicationVariant -import com.android.build.api.variant.impl.ApplicationVariantImpl -import com.android.build.gradle.BaseExtension import com.android.ide.common.signing.KeystoreHelper -import org.jetbrains.kotlin.konan.properties.Properties +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.PrintStream - -val minSdkVer: Int by rootProject.extra -val targetSdkVer: Int by rootProject.extra - -val appVerName: String by rootProject.extra -val appVerCode: Int by rootProject.extra -val serviceVer: Int by rootProject.extra -val minExtensionVer: Int by rootProject.extra -val minBackupVer: Int by rootProject.extra - -val gitCommitCount: String by rootProject.extra -val gitCommitHash: String by rootProject.extra - -val properties = Properties() -properties.load(project.rootProject.file("local.properties").inputStream()) +import java.util.* plugins { id("com.android.application") id("com.google.gms.google-services") + id("dev.rikka.tools.refine") kotlin("android") } android { - compileSdk = targetSdkVer + namespace = "com.tsng.hidemyapplist" buildFeatures { viewBinding = true } +} - defaultConfig { - applicationId = "com.tsng.hidemyapplist" - versionCode = appVerCode - versionName = appVerName - minSdk = minSdkVer - targetSdk = targetSdkVer - - multiDexEnabled = false - if (properties.getProperty("buildWithGitSuffix").toBoolean()) - versionNameSuffix = ".r${gitCommitCount}.${gitCommitHash}" - - buildConfigField("int", "SERVICE_VERSION", serviceVer.toString()) - buildConfigField("int", "MIN_EXTENSION_VERSION", minExtensionVer.toString()) - buildConfigField("int", "MIN_BACKUP_VERSION", minBackupVer.toString()) - } - - signingConfigs.create("config") { - storeFile = file(properties.getProperty("fileDir")) - storePassword = properties.getProperty("storePassword") - keyAlias = properties.getProperty("keyAlias") - keyPassword = properties.getProperty("keyPassword") - } - - buildTypes { - signingConfigs.named("config").get().also { - debug { - signingConfig = it - } - release { - signingConfig = it - isMinifyEnabled = true - isShrinkResources = true - proguardFiles("proguard-rules.pro") +fun afterEval() = android.applicationVariants.forEach { variant -> + val variantCapped = variant.name.capitalize(Locale.ROOT) + val variantLowered = variant.name.toLowerCase(Locale.ROOT) + + val outSrcDir = file("$buildDir/generated/source/signInfo/${variantLowered}") + val outSrc = file("$outSrcDir/com/tsng/hidemyapplist/Magic.java") + val signInfoTask = task("generate${variantCapped}SignInfo") { + dependsOn("validateSigning${variantCapped}") + outputs.file(outSrc) + doLast { + val sign = android.buildTypes[variantLowered].signingConfig + outSrc.parentFile.mkdirs() + val certificateInfo = KeystoreHelper.getCertificateInfo( + sign?.storeType, + sign?.storeFile, + sign?.storePassword, + sign?.keyPassword, + sign?.keyAlias + ) + PrintStream(outSrc).apply { + println("package com.tsng.hidemyapplist;") + println("public final class Magic {") + print("public static final byte[] magicNumbers = {") + val bytes = certificateInfo.certificate.encoded + print(bytes.joinToString(",") { it.toString() }) + println("};") + println("}") } } } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + variant.registerJavaGeneratingTask(signInfoTask, arrayListOf(outSrcDir)) + + val kotlinCompileTask = tasks.findByName("compile${variantCapped}Kotlin") as KotlinCompile + kotlinCompileTask.dependsOn(signInfoTask) + val srcSet = objects.sourceDirectorySet("magic", "magic").srcDir(outSrcDir) + kotlinCompileTask.source(srcSet) + + task("build$variantCapped") { + dependsOn("assemble$variantCapped") + from("$buildDir/outputs/apk/$variantLowered") + into("$buildDir/apk/$variantLowered") + rename(".*.apk", "HMA-V${variant.versionName}-${variant.buildType.name}.apk") } } -// This code is forked from LSPosed -// Make a class containing a byte array of signature -androidComponents.onVariants { v -> - val variant: ApplicationVariantImpl = - if (v is ApplicationVariantImpl) v - else (v as AnalyticsEnabledApplicationVariant).delegate as ApplicationVariantImpl - val variantCapped = variant.name.capitalize() - val variantLowered = variant.name.toLowerCase() - - variant.outputs.forEach { - it.outputFileName.set("V${it.versionName.get()}-${variant.buildType}.apk") - } - - afterEvaluate { - val app = rootProject.project(":app").extensions.getByName("android") - val outSrcDir = file("$buildDir/generated/source/signInfo/${variantLowered}") - val outSrc = file("$outSrcDir/com/tsng/hidemyapplist/Magic.java") - val signInfoTask = tasks.register("generate${variantCapped}SignInfo") { - dependsOn(":app:validateSigning${variantCapped}") - outputs.file(outSrc) - doLast { - val sign = app.buildTypes.named(variantLowered).get().signingConfig - outSrc.parentFile.mkdirs() - val certificateInfo = KeystoreHelper.getCertificateInfo( - sign?.storeType, - sign?.storeFile, - sign?.storePassword, - sign?.keyPassword, - sign?.keyAlias - ) - PrintStream(outSrc).apply { - println("package com.tsng.hidemyapplist;") - println("public final class Magic {") - print("public static final byte[] magicNumbers = {") - val bytes = certificateInfo.certificate.encoded - print(bytes.joinToString(",") { it.toString() }) - println("};") - println("}") - } - } - } - variant.variantData.registerJavaGeneratingTask(signInfoTask, arrayListOf(outSrcDir)) - - val kotlinCompileTask = - tasks.findByName("compile${variant.name.capitalize()}Kotlin") as? SourceTask - if (kotlinCompileTask != null) { - kotlinCompileTask.dependsOn(signInfoTask) - val srcSet = objects.sourceDirectorySet("magic", "magic").srcDir(outSrcDir) - kotlinCompileTask.source(srcSet) - } - } +afterEvaluate { + afterEval() } dependencies { - implementation("com.drakeet.about:about:2.5.0") - implementation("com.drakeet.multitype:multitype:4.3.0") - implementation("com.scwang.smart:refresh-layout-kernel:2.0.3") - implementation("com.scwang.smart:refresh-header-material:2.0.3") - implementation("com.github.kyuubiran:EzXHelper:0.6.1") - implementation("com.github.topjohnwu.libsu:core:3.1.2") + implementation(projects.common) + runtimeOnly(projects.xposed) - implementation("com.google.code.gson:gson:2.8.9") - implementation("com.google.android.material:material:1.5.0") - implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.3") - implementation("androidx.appcompat:appcompat:1.4.1") - implementation("androidx.preference:preference-ktx:1.2.0") + implementation("androidx.appcompat:appcompat:1.4.2") implementation("androidx.fragment:fragment-ktx:1.4.1") + implementation("androidx.preference:preference-ktx:1.2.0") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.work:work-runtime-ktx:2.7.1") - implementation("com.google.android.gms:play-services-ads:20.5.0") - implementation("com.google.firebase:firebase-analytics-ktx:20.1.0") - - compileOnly("de.robv.android.xposed:api:82") - compileOnly("de.robv.android.xposed:api:82:sources") + implementation("com.drakeet.about:about:2.5.1") + implementation("com.drakeet.multitype:multitype:4.3.0") + implementation("com.github.topjohnwu.libsu:core:3.1.2") + implementation("com.google.android.material:material:1.6.1") + implementation("com.google.android.gms:play-services-ads:21.0.0") + implementation("com.google.firebase:firebase-analytics-ktx:21.0.0") + implementation("com.squareup.okhttp3:okhttp:4.9.1") + implementation("dev.rikka.hidden:compat:2.3.1") + compileOnly("dev.rikka.hidden:stub:2.3.1") } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0abc1f2a..0aee042c 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,24 +1,3 @@ -# 指定压缩级别 --optimizationpasses 5 -# 混淆时采用的算法 --optimizations !code/simplification/arithmetic, !field,!class/merging, !code/allocation/variable -# 优化时允许访问并修改有修饰符的类和类的成员 --allowaccessmodification -# 将文件来源重命名为“SourceFile”字符串 --renamesourcefileattribute SourceFile -# 保持异常不被混淆 --keepattributes Exceptions -# 保留行号 --keepattributes SourceFile, LineNumberTable -# 保留注解不混淆 --keepattributes *Annotation*,InnerClasses -# 保持泛型 --keepattributes Signature -# 保持反射 --keepattributes EnclosingMethod -# 保留 native 方法的类名和方法名 --keepclasseswithmembernames class * { native ; } - # Magic -keep class com.tsng.hidemyapplist.Magic { *; } -keep class com.tsng.hidemyapplist.app.MyApplication { @@ -30,13 +9,7 @@ -keep class com.tsng.hidemyapplist.app.ui.activities.ModuleActivity$Fragment { *; } -keep class com.tsng.hidemyapplist.app.ui.views.FilterRulesView{ *; } -# Config --keep class com.tsng.hidemyapplist.JsonConfig { *; } --keep class com.tsng.hidemyapplist.JsonConfig$* { *; } - # Xposed --keep class com.tsng.hidemyapplist.xposed.XposedEntry --keep class com.tsng.hidemyapplist.xposed.PackageManagerService -keepclassmembers class com.tsng.hidemyapplist.app.MyApplication { static final boolean isModuleActivated; } @@ -46,12 +19,3 @@ public static **[] values(); public static ** valueOf(java.lang.String); } - -# Dontwarn --dontwarn org.bouncycastle.jsse.BCSSLParameters --dontwarn org.bouncycastle.jsse.BCSSLSocket --dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider --dontwarn org.conscrypt.Conscrypt* --dontwarn org.openjsse.javax.net.ssl.SSLParameters --dontwarn org.openjsse.javax.net.ssl.SSLSocket --dontwarn org.openjsse.net.ssl.OpenJSSE \ No newline at end of file diff --git a/app/src/main/assets/extension.zip b/app/src/main/assets/extension.zip deleted file mode 100644 index f975c3f7..00000000 Binary files a/app/src/main/assets/extension.zip and /dev/null differ diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init index 59b1a101..013e2141 100644 --- a/app/src/main/assets/xposed_init +++ b/app/src/main/assets/xposed_init @@ -1 +1 @@ -com.tsng.hidemyapplist.xposed.XposedEntry \ No newline at end of file +icu.nullptr.hidemyapplist.xposed.XposedEntry diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/AppUtils.kt b/app/src/main/java/com/tsng/hidemyapplist/app/AppUtils.kt index 98b3adae..850656a7 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/AppUtils.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/AppUtils.kt @@ -5,9 +5,11 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction -import com.google.gson.Gson import com.tsng.hidemyapplist.R import com.tsng.hidemyapplist.app.MyApplication.Companion.appContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json fun makeToast(@StringRes resId: Int) { Toast.makeText(appContext, resId, Toast.LENGTH_SHORT).show() @@ -17,7 +19,7 @@ fun makeToast(text: CharSequence) { Toast.makeText(appContext, text, Toast.LENGTH_SHORT).show() } -fun T.deepCopy(): T = Gson().fromJson(Gson().toJson(this), this::class.java) +inline fun T.deepCopy(): T = Json.decodeFromString(Json.encodeToString(this)) fun AppCompatActivity.startFragment(fragment: Fragment, addToBackStack: Boolean = true) { val transaction = supportFragmentManager @@ -36,4 +38,4 @@ fun Fragment.startFragment(fragment: Fragment, addToBackStack: Boolean = true) { .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) if (addToBackStack) transaction.addToBackStack(null) transaction.commit() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/JsonConfigManager.kt b/app/src/main/java/com/tsng/hidemyapplist/app/JsonConfigManager.kt index 3eccee7b..daa68941 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/JsonConfigManager.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/JsonConfigManager.kt @@ -2,21 +2,23 @@ package com.tsng.hidemyapplist.app import com.tsng.hidemyapplist.BuildConfig -import com.tsng.hidemyapplist.JsonConfig import com.tsng.hidemyapplist.R import com.tsng.hidemyapplist.app.MyApplication.Companion.appContext +import icu.nullptr.hidemyapplist.common.JsonConfig +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.io.File - object JsonConfigManager { val configFile = File("${appContext.filesDir.absolutePath}/config.json") val globalConfig: JsonConfig init { if (!configFile.exists()) - configFile.writeText(JsonConfig().toString()) + configFile.writeText(Json.encodeToString(JsonConfig())) try { - globalConfig = JsonConfig.fromJson(configFile.readText()) + globalConfig = Json.decodeFromString(configFile.readText()) val configVersion = globalConfig.configVersion if (configVersion < 49) throw RuntimeException("Config version too old") if (configVersion < 65) migrateFromPre65() @@ -52,4 +54,4 @@ object JsonConfigManager { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/helpers/PreferenceDataStorage.kt b/app/src/main/java/com/tsng/hidemyapplist/app/helpers/PreferenceDataStorage.kt index dc1925ce..f5cfd559 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/helpers/PreferenceDataStorage.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/helpers/PreferenceDataStorage.kt @@ -1,33 +1,27 @@ package com.tsng.hidemyapplist.app.helpers import androidx.preference.PreferenceDataStore -import com.github.kyuubiran.ezxhelper.utils.getFieldByClassOrObject -import com.tsng.hidemyapplist.JsonConfig.AppConfig -import com.tsng.hidemyapplist.JsonConfig.Template +import icu.nullptr.hidemyapplist.common.JsonConfig -class TemplateDataStorage(private val template: Template) : PreferenceDataStore() { - -} - -class AppConfigDataStorage(private val appConfig: AppConfig) : PreferenceDataStore() { +class AppConfigDataStorage(private val appConfig: JsonConfig.AppConfig) : PreferenceDataStore() { var isEnabled = false override fun getBoolean(key: String, defValue: Boolean): Boolean { return if (key == "isEnabled") isEnabled - else AppConfig::class.java.getFieldByClassOrObject(key).getBoolean(appConfig) + else JsonConfig.AppConfig::class.java.getField(key).getBoolean(appConfig) } override fun putBoolean(key: String, value: Boolean) { if (key == "isEnabled") isEnabled = value - else AppConfig::class.java.getFieldByClassOrObject(key).setBoolean(appConfig, value) + else JsonConfig.AppConfig::class.java.getField(key).setBoolean(appConfig, value) } override fun getStringSet(key: String, defValues: MutableSet?): MutableSet { - return AppConfig::class.java.getFieldByClassOrObject(key) + return JsonConfig.AppConfig::class.java.getField(key) .get(appConfig) as MutableSet } override fun putStringSet(key: String, values: MutableSet?) { - AppConfig::class.java.getFieldByClassOrObject(key).set(appConfig, values) + JsonConfig.AppConfig::class.java.getField(key).set(appConfig, values) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/helpers/ServiceHelper.kt b/app/src/main/java/com/tsng/hidemyapplist/app/helpers/ServiceHelper.kt index 58ff7be3..b91de2e6 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/helpers/ServiceHelper.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/helpers/ServiceHelper.kt @@ -1,65 +1,50 @@ package com.tsng.hidemyapplist.app.helpers -import com.tsng.hidemyapplist.app.MyApplication.Companion.appContext +import android.os.Parcel +import android.os.RemoteException +import android.os.ServiceManager +import android.util.Log +import icu.nullptr.hidemyapplist.common.Constants +import icu.nullptr.hidemyapplist.common.IHMAService object ServiceHelper { - @JvmStatic - fun getServiceVersion(): Int { - return try { - appContext.packageManager.getInstallerPackageName("getServiceVersion")!!.toInt() - } catch (e: IllegalArgumentException) { - 0 - } - } - - @JvmStatic - fun getExtensionVersion(): Int { - return try { - appContext.packageManager.getInstallerPackageName("getExtensionVersion")!!.toInt() - } catch (e: IllegalArgumentException) { - 0 - } - } - @JvmStatic - fun getServeTimes(): Int { - return try { - appContext.packageManager.getInstallerPackageName("getServeTimes")!!.toInt() - } catch (e: IllegalArgumentException) { - 0 - } - } + private const val TAG = "ServiceHelper" - @JvmStatic - fun getLogs(): String? { + private fun getService(): IHMAService? { + val pm = ServiceManager.getService("package") + val data = Parcel.obtain() + val reply = Parcel.obtain() return try { - appContext.packageManager.getInstallerPackageName("getLogs") - } catch (e: IllegalArgumentException) { + data.enforceInterface(Constants.DESCRIPTOR) + data.writeInt(Constants.ACTION_GET_BINDER) + pm.transact(Constants.TRANSACTION, data, reply, 0) + val binder = reply.readStrongBinder() + IHMAService.Stub.asInterface(binder) + } catch (e: RemoteException) { + Log.d(TAG, "Failed to get binder") null + } finally { + data.recycle() + reply.recycle() } } - @JvmStatic + fun getServiceVersion() = getService()?.serviceVersion ?: 0 + + fun getServeTimes() = getService()?.filterCount ?: 0 + + fun getLogs() = getService()?.logs + fun cleanLogs() { - try { - appContext.packageManager.getInstallerPackageName("cleanLogs") - } catch (e: IllegalArgumentException) { - } + getService()?.clearLogs() } - @JvmStatic fun submitConfig(json: String) { - try { - appContext.packageManager.getInstallerPackageName("submitConfig#$json") - } catch (e: IllegalArgumentException) { - } + getService()?.syncConfig(json) } - @JvmStatic fun stopSystemService(cleanEnv: Boolean) { - try { - appContext.packageManager.getInstallerPackageName("stopSystemService#$cleanEnv") - } catch (e: IllegalArgumentException) { - } + getService()?.stopService(cleanEnv) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/ui/activities/AboutActivity.kt b/app/src/main/java/com/tsng/hidemyapplist/app/ui/activities/AboutActivity.kt index c065a5e3..27f8e119 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/ui/activities/AboutActivity.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/ui/activities/AboutActivity.kt @@ -24,8 +24,6 @@ class AboutActivity : AbsAboutActivity() { items.add(Card(getString(R.string.about_how_to_use_description_1))) items.add(Line()) items.add(Card(getString(R.string.about_how_to_use_description_2))) - items.add(Line()) - items.add(Card(getString(R.string.about_how_to_use_description_3))) items.add(Category(getString(R.string.about_hook_differences_title))) items.add(Card(getString(R.string.about_hook_differences_description))) @@ -52,8 +50,6 @@ class AboutActivity : AbsAboutActivity() { items.add(License("SmartRefreshLayout", "scwang90", License.APACHE_2, "https://github.com/scwang90/SmartRefreshLayout")) items.add(License("EzXHelper", "KyuubiRan", License.APACHE_2, "https://github.com/KyuubiRan/EzXHelper")) items.add(License("libsu", "topjohnwu", License.APACHE_2, "https://github.com/topjohnwu/libsu")) - items.add(License("Gson", "Google", License.APACHE_2, "https://github.com/google/gson")) items.add(License("okhttp", "square", License.APACHE_2, "https://github.com/square/okhttp")) - items.add(License("linux-syscall-support", "Google", "Google", "https://chromium.googlesource.com/linux-syscall-support")) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/ui/activities/MainActivity.kt b/app/src/main/java/com/tsng/hidemyapplist/app/ui/activities/MainActivity.kt index 5f950fa9..3b0fbde0 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/ui/activities/MainActivity.kt @@ -1,7 +1,6 @@ package com.tsng.hidemyapplist.app.ui.activities import android.annotation.SuppressLint -import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -23,12 +22,10 @@ import com.tsng.hidemyapplist.app.MyApplication.Companion.appContext import com.tsng.hidemyapplist.app.SubmitConfigService import com.tsng.hidemyapplist.app.helpers.ServiceHelper import com.tsng.hidemyapplist.app.makeToast -import com.tsng.hidemyapplist.app.ui.views.ShellDialog import com.tsng.hidemyapplist.databinding.ActivityMainBinding import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONObject -import java.io.File import java.util.* import kotlin.concurrent.thread @@ -53,16 +50,12 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { binding = ActivityMainBinding.inflate(layoutInflater) binding.menuDetectionTest.setOnClickListener(this) - binding.menuInstallExtension.setOnClickListener(this) binding.menuTemplateManage.setOnClickListener(this) binding.menuScopeManage.setOnClickListener(this) binding.menuLogs.setOnClickListener(this) binding.menuSettings.setOnClickListener(this) binding.menuAbout.setOnClickListener(this) - if (MyApplication.isModuleActivated) - binding.menuInstallExtension.visibility = View.VISIBLE - setContentView(binding.root) setSupportActionBar(findViewById(R.id.toolbar)) MobileAds.initialize(appContext) @@ -92,7 +85,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { if (serviceVersion != 0) { binding.serviceStatusText.text = - if (serviceVersion != BuildConfig.SERVICE_VERSION) + if (serviceVersion != icu.nullptr.hidemyapplist.common.BuildConfig.SERVICE_VERSION) getString(R.string.xposed_service_old) else getString(R.string.xposed_service_on) + " [$serviceVersion]" @@ -100,13 +93,6 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { binding.serveTimes.visibility = View.VISIBLE binding.serveTimes.text = text[0] + ServiceHelper.getServeTimes() + text[2] - binding.extensionStatusText.visibility = View.VISIBLE - binding.extensionStatusText.text = when (val extensionVersion = ServiceHelper.getExtensionVersion()) { - 0 -> getString(R.string.extension_not_installed) - -1 -> getString(R.string.extension_version_too_old) - -2 -> getString(R.string.extension_apk_version_too_old) - else -> getString(R.string.extension_installed) + " [$extensionVersion]" - } } else { binding.serveTimes.visibility = View.GONE binding.serviceStatusText.text = getString(R.string.xposed_service_off) @@ -115,29 +101,6 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { override fun onClick(v: View) { when (v.id) { - R.id.menu_install_extension -> { - val listener: (DialogInterface, Int) -> Unit = { dialog, which -> - val flavor = if (which == DialogInterface.BUTTON_POSITIVE) "Zygisk" else "Riru" - val zipFile = File("$cacheDir/$flavor-HideMyApplist.zip") - assets.open("extension.zip").use { fis -> - zipFile.outputStream().use { - fis.copyTo(it) - } - } - - dialog.dismiss() - ShellDialog(this) - .setCommands("su --mount-master -c magisk --install-module ${zipFile.absolutePath}") - .create() - } - MaterialAlertDialogBuilder(this) - .setTitle(R.string.install_magisk_extension_title) - .setMessage(R.string.install_magisk_extension_message) - .setNeutralButton(android.R.string.cancel, null) - .setNegativeButton("Riru", listener) - .setPositiveButton("Zygisk", listener) - .show() - } R.id.menu_detection_test -> { val intent = packageManager.getLaunchIntentForPackage("icu.nullptr.applistdetector") if (intent == null) { diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/AppSelectFragment.kt b/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/AppSelectFragment.kt index b5abb42c..bf74244a 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/AppSelectFragment.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/AppSelectFragment.kt @@ -6,13 +6,16 @@ import androidx.appcompat.widget.SearchView import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import com.github.kyuubiran.ezxhelper.utils.runOnMainThread import com.tsng.hidemyapplist.R import com.tsng.hidemyapplist.app.helpers.AppInfoHelper import com.tsng.hidemyapplist.app.ui.adapters.AppSelectAdapter import com.tsng.hidemyapplist.app.ui.views.Ads import com.tsng.hidemyapplist.databinding.FragmentAppSelectBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.text.Collator import java.util.* import kotlin.concurrent.thread @@ -51,7 +54,11 @@ class AppSelectFragment : Fragment() { ): View { binding = FragmentAppSelectBinding.inflate(inflater, container, false) binding.adBanner.loadAd(Ads.appSelectAd) - binding.refreshLayout.setOnRefreshListener { refresh() }.autoRefresh() + binding.refreshLayout.setOnRefreshListener { + lifecycleScope.launch { refresh() } + } + binding.refreshLayout.isRefreshing = true + lifecycleScope.launch { refresh() } return binding.root } @@ -89,14 +96,14 @@ class AppSelectFragment : Fragment() { return true } - private fun refresh() { - thread { - initAppListView() - runOnMainThread { binding.refreshLayout.finishRefresh() } + private suspend fun refresh() { + initAppListView() + withContext(Dispatchers.Main) { + binding.refreshLayout.isRefreshing = false } } - private fun initAppListView() { + private suspend fun initAppListView() { val appInfoList = AppInfoHelper.getAppInfoList() appInfoList.sortWith { o1, o2 -> val c1 = selectedApps.contains(o1.packageName) @@ -104,10 +111,10 @@ class AppSelectFragment : Fragment() { if (c1 != c2) return@sortWith if (c1) -1 else 1 Collator.getInstance(Locale.getDefault()).compare(o1.appName, o2.appName) } - runOnMainThread { + withContext(Dispatchers.Main) { binding.appSelect.layoutManager = LinearLayoutManager(activity) adapter = AppSelectAdapter(isShowSystemApp, true, appInfoList, selectedApps) binding.appSelect.adapter = adapter } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/AppSettingsFragment.kt b/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/AppSettingsFragment.kt index 64094fbd..e019c4db 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/AppSettingsFragment.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/AppSettingsFragment.kt @@ -12,7 +12,6 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tsng.hidemyapplist.JsonConfig import com.tsng.hidemyapplist.R import com.tsng.hidemyapplist.app.JsonConfigManager import com.tsng.hidemyapplist.app.JsonConfigManager.globalConfig @@ -22,6 +21,7 @@ import com.tsng.hidemyapplist.app.helpers.AppConfigDataStorage import com.tsng.hidemyapplist.app.makeToast import com.tsng.hidemyapplist.app.startFragment import com.tsng.hidemyapplist.app.ui.views.FilterRulesView +import icu.nullptr.hidemyapplist.common.JsonConfig class AppSettingsFragment : PreferenceFragmentCompat() { companion object { @@ -183,4 +183,4 @@ class AppSettingsFragment : PreferenceFragmentCompat() { else scope.remove(packageName) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/ScopeManageFragment.kt b/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/ScopeManageFragment.kt index 263a1a30..17f6256a 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/ScopeManageFragment.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/ScopeManageFragment.kt @@ -4,8 +4,8 @@ import android.os.Bundle import android.view.* import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import com.github.kyuubiran.ezxhelper.utils.runOnMainThread import com.tsng.hidemyapplist.BuildConfig import com.tsng.hidemyapplist.R import com.tsng.hidemyapplist.app.JsonConfigManager.globalConfig @@ -14,9 +14,11 @@ import com.tsng.hidemyapplist.app.startFragment import com.tsng.hidemyapplist.app.ui.adapters.AppSelectAdapter import com.tsng.hidemyapplist.app.ui.views.Ads import com.tsng.hidemyapplist.databinding.FragmentAppSelectBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.text.Collator import java.util.* -import kotlin.concurrent.thread class ScopeManageFragment : Fragment() { @@ -37,7 +39,11 @@ class ScopeManageFragment : Fragment() { ): View { binding = FragmentAppSelectBinding.inflate(inflater, container, false) binding.adBanner.loadAd(Ads.appSelectAd) - binding.refreshLayout.setOnRefreshListener { refresh() }.autoRefresh() + binding.refreshLayout.setOnRefreshListener { + lifecycleScope.launch { refresh() } + } + binding.refreshLayout.isRefreshing = true + lifecycleScope.launch { refresh() } return binding.root } @@ -72,14 +78,14 @@ class ScopeManageFragment : Fragment() { return true } - private fun refresh() { - thread { + private suspend fun refresh() { initAppListView() - runOnMainThread { binding.refreshLayout.finishRefresh() } - } + withContext(Dispatchers.Main) { + binding.refreshLayout.isRefreshing = false + } } - private fun initAppListView() { + private suspend fun initAppListView() { val appInfoList = AppInfoHelper.getAppInfoList() appInfoList.removeIf { it.packageName == BuildConfig.APPLICATION_ID } val selectedApps = globalConfig.scope.keys @@ -89,7 +95,7 @@ class ScopeManageFragment : Fragment() { if (b1 != b2) return@sortWith if (b1) -1 else 1 Collator.getInstance(Locale.getDefault()).compare(o1.appName, o2.appName) } - runOnMainThread { + withContext(Dispatchers.Main) { binding.appSelect.layoutManager = LinearLayoutManager(activity) adapter = AppSelectAdapter(isShowSystemApp, false, appInfoList, selectedApps) { itemView.setOnClickListener { @@ -101,4 +107,4 @@ class ScopeManageFragment : Fragment() { binding.appSelect.adapter = adapter } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/SettingsFragment.kt b/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/SettingsFragment.kt index d2cb5ec6..1df11247 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/SettingsFragment.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/SettingsFragment.kt @@ -9,19 +9,19 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.gson.Gson -import com.google.gson.JsonObject -import com.google.gson.JsonParser import com.topjohnwu.superuser.Shell -import com.tsng.hidemyapplist.BuildConfig -import com.tsng.hidemyapplist.JsonConfig import com.tsng.hidemyapplist.R import com.tsng.hidemyapplist.app.JsonConfigManager import com.tsng.hidemyapplist.app.MyApplication.Companion.appContext import com.tsng.hidemyapplist.app.helpers.ServiceHelper import com.tsng.hidemyapplist.app.makeToast +import icu.nullptr.hidemyapplist.common.BuildConfig +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject class SettingsFragment : PreferenceFragmentCompat() { + private val backupImportSAFLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri == null) return@registerForActivityResult @@ -32,23 +32,24 @@ class SettingsFragment : PreferenceFragmentCompat() { val backupJson: JsonObject val backupVersion: Int try { - backupJson = JsonParser.parseString(backup).asJsonObject - backupVersion = backupJson["configVersion"].asInt + + backupJson = Json.parseToJsonElement(backup) as JsonObject + backupVersion = backupJson["configVersion"].toString().toInt() } catch (e: Exception) { throw RuntimeException(getString(R.string.settings_import_file_damaged)) .apply { addSuppressed(e) } } - if (backupVersion > BuildConfig.VERSION_CODE) + if (backupVersion > BuildConfig.SERVICE_VERSION) throw RuntimeException(getString(R.string.settings_import_app_version_too_old)) if (backupVersion < BuildConfig.MIN_BACKUP_VERSION) throw RuntimeException(getString(R.string.settings_import_backup_version_too_old)) JsonConfigManager.edit { templates.clear() - for ((name, template) in backupJson["templates"].asJsonObject.entrySet()) - templates[name] = Gson().fromJson(template.toString(), JsonConfig.Template::class.java) + for ((name, template) in backupJson["templates"] as JsonObject) + templates[name] = Json.decodeFromString(template.toString()) scope.clear() - for ((name, appConfig) in backupJson["scope"].asJsonObject.entrySet()) - scope[name] = Gson().fromJson(appConfig.toString(), JsonConfig.AppConfig::class.java) + for ((name, appConfig) in backupJson["scope"] as JsonObject) + scope[name] = Json.decodeFromString(appConfig.toString()) } makeToast(R.string.settings_import_successful) } catch (e: Exception) { @@ -171,4 +172,4 @@ class SettingsFragment : PreferenceFragmentCompat() { true } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/TemplateSettingsFragment.kt b/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/TemplateSettingsFragment.kt index 75af0f5c..7be4cb70 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/TemplateSettingsFragment.kt +++ b/app/src/main/java/com/tsng/hidemyapplist/app/ui/fragments/TemplateSettingsFragment.kt @@ -6,7 +6,6 @@ import android.view.* import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResultListener import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tsng.hidemyapplist.JsonConfig import com.tsng.hidemyapplist.R import com.tsng.hidemyapplist.app.JsonConfigManager import com.tsng.hidemyapplist.app.JsonConfigManager.globalConfig @@ -15,6 +14,7 @@ import com.tsng.hidemyapplist.app.makeToast import com.tsng.hidemyapplist.app.startFragment import com.tsng.hidemyapplist.app.ui.views.FilterRulesView import com.tsng.hidemyapplist.databinding.FragmentTemplateSettingsBinding +import icu.nullptr.hidemyapplist.common.JsonConfig class TemplateSettingsFragment : Fragment() { companion object { @@ -156,4 +156,4 @@ class TemplateSettingsFragment : Fragment() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/app/ui/views/ShellDialog.kt b/app/src/main/java/com/tsng/hidemyapplist/app/ui/views/ShellDialog.kt deleted file mode 100644 index 5d047831..00000000 --- a/app/src/main/java/com/tsng/hidemyapplist/app/ui/views/ShellDialog.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.tsng.hidemyapplist.app.ui.views - -import android.app.Dialog -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import androidx.annotation.StringRes -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.topjohnwu.superuser.CallbackList -import com.topjohnwu.superuser.Shell -import com.tsng.hidemyapplist.R -import com.tsng.hidemyapplist.databinding.ShellDialogBinding - - -class ShellDialog(private val context: Context) { - - private val binding = ShellDialogBinding.inflate(LayoutInflater.from(context)) - private val console = object : CallbackList() { - override fun onAddElement(s: String?) { - binding.console.append("$s\n") - } - } - - private var commands: Array? = null - private var successfulMainButtonText = context.getText(android.R.string.ok) - private var successfulViceButtonText: CharSequence? = null - private var failedButtonText = context.getText(android.R.string.ok) - private var successfulMainButtonClickListener: View.OnClickListener? = null - private var successfulViceButtonClickListener: View.OnClickListener? = null - private var failedButtonClickListener: View.OnClickListener? = null - - fun setCommands(vararg cmd: String): ShellDialog { - commands = cmd - return this - } - - fun setSuccessfulMainButton(@StringRes ResId: Int?, onClickListener: View.OnClickListener?): ShellDialog { - ResId?.let { successfulMainButtonText = context.getText(it) } - successfulMainButtonClickListener = onClickListener - return this - } - - fun setSuccessfulViceButton(@StringRes ResId: Int?, onClickListener: View.OnClickListener?): ShellDialog { - successfulViceButtonText = context.getText(ResId ?: android.R.string.cancel) - successfulViceButtonClickListener = onClickListener - return this - } - - fun setFailedButton(@StringRes ResId: Int?, onClickListener: View.OnClickListener?): ShellDialog { - ResId?.let { failedButtonText = context.getText(it) } - failedButtonClickListener = onClickListener - return this - } - - fun create() { - commands ?: throw IllegalArgumentException("Commands not set") - - val dialog = MaterialAlertDialogBuilder(context) - .setTitle(R.string.shell) - .setView(binding.root) - .setCancelable(false) - .setPositiveButton("Stub!", null) - .setNegativeButton("Stub!", null) - .create() - .apply { - create() - getButton(Dialog.BUTTON_POSITIVE).visibility = View.GONE - getButton(Dialog.BUTTON_NEGATIVE).visibility = View.GONE - show() - } - - if (!Shell.getShell().isRoot) { - console.add(context.getString(R.string.no_root_permission)) - dialog.getButton(Dialog.BUTTON_POSITIVE).apply { - visibility = View.VISIBLE - text = failedButtonText - setOnClickListener { failedButtonClickListener; dialog.dismiss() } - } - return - } - - Shell.su(*commands!!).to(console, console).submit { out: Shell.Result -> - console.add("------------") - console.add("result code: ${out.code}") - if (out.isSuccess) { - console.add(context.getString(R.string.execute_successfully)) - dialog.getButton(Dialog.BUTTON_POSITIVE).apply { - visibility = View.VISIBLE - text = successfulMainButtonText - setOnClickListener { successfulMainButtonClickListener?.onClick(it); dialog.dismiss() } - } - if (successfulViceButtonText != null) - dialog.getButton(Dialog.BUTTON_NEGATIVE).apply { - visibility = View.VISIBLE - text = successfulViceButtonText - setOnClickListener { successfulViceButtonClickListener?.onClick(it); dialog.dismiss() } - } - } else { - console.add(context.getString(R.string.execute_failed)) - dialog.getButton(Dialog.BUTTON_POSITIVE).apply { - visibility = View.VISIBLE - text = failedButtonText - setOnClickListener { failedButtonClickListener?.onClick(it); dialog.dismiss() } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tsng/hidemyapplist/xposed/PackageManagerService.kt b/app/src/main/java/com/tsng/hidemyapplist/xposed/PackageManagerService.kt deleted file mode 100644 index bc106870..00000000 --- a/app/src/main/java/com/tsng/hidemyapplist/xposed/PackageManagerService.kt +++ /dev/null @@ -1,493 +0,0 @@ -/* - * This file is part of Hide My Applist. - * - * Hide My Applist is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with Hide My Applist. - * If not, see . - * - * Copyright (C) 2021 Hide My Applist Contributors - */ - -package com.tsng.hidemyapplist.xposed - -import android.content.ComponentName -import android.content.Intent -import android.content.pm.ApplicationInfo -import com.github.kyuubiran.ezxhelper.utils.* -import com.tsng.hidemyapplist.BuildConfig -import com.tsng.hidemyapplist.JsonConfig -import com.tsng.hidemyapplist.xposed.ServiceUtils.getBinderCaller -import com.tsng.hidemyapplist.xposed.ServiceUtils.getRecursiveField -import de.robv.android.xposed.XC_MethodHook -import de.robv.android.xposed.XposedBridge -import java.io.File -import java.io.FileNotFoundException -import java.lang.reflect.Method -import java.text.SimpleDateFormat -import java.util.* -import kotlin.concurrent.thread - -object PackageManagerService { - private const val hmaApp = "com.tsng.hidemyapplist" - - private val customPms = arrayOf( - "com.android.server.pm.OplusPackageManagerService", - "com.android.server.pm.OppoPackageManagerService" - ) - - private val allHooks = mutableSetOf() - private val systemApps = mutableSetOf() - private var stopped = false - private var configCached = false - private var extensionVersion = 0 - private var mLock = Any() - - private lateinit var dataDir: String - private lateinit var logFile: File - private lateinit var token: String - - @Volatile - private var config = JsonConfig() - - @Volatile - private var configStr = JsonConfig().toString() - - @Volatile - private var interceptionCount = 0 - - private object Log { - fun d(msg: String) { - if (!config.detailLog) return - val s = "[HMA Xposed] [DEBUG] $msg" - XposedBridge.log(s) - addLog(s) - } - - fun i(msg: String) { - val s = "[HMA Xposed] [INFO] $msg" - XposedBridge.log(s) - addLog(s) - } - - fun e(msg: String) { - val s = "[HMA Xposed] [ERROR] $msg" - XposedBridge.log(s) - addLog(s) - } - } - - private fun generateRandomString(length: Int): String { - val leftLimit = 97 // letter 'a' - val rightLimit = 122 // letter 'z' - val random = Random() - val buffer = StringBuilder(length) - for (i in 0 until length) { - val randomLimitedInt = leftLimit + (random.nextFloat() * (rightLimit - leftLimit + 1)).toInt() - buffer.append(randomLimitedInt.toChar()) - } - return buffer.toString() - } - - private fun generateToken() { - token = generateRandomString(10) - File("$dataDir/tmp/token").writeText(token) - } - - private fun addLog(log: String) { - synchronized(mLock) { - if (logFile.length() / 1024 > config.maxLogSize) logFile.delete() - val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss ", Locale.getDefault()).format(Date()) - logFile.appendText(date + log + '\n') - } - } - - private fun provideLogs(): String { - try { - synchronized(mLock) { - if (stopped) throw InterruptedException("Service stopped") - val sb = StringBuilder() - val list = logFile.readLines() - for (line in list) sb.append(line).append("\n") - return sb.toString() - } - } catch (e: Exception) { - return "Failed to load logs\n" + e.stackTraceToString() - } - } - - private fun initConfig() { - configStr = File("$dataDir/config.json").readText() - config = JsonConfig.fromJson(configStr) - if (config.configVersion < BuildConfig.SERVICE_VERSION) { - config = JsonConfig() - Log.i("Config cache version too old, need refresh") - return - } - Log.d("Cached config: $config") - configCached = true - try { - interceptionCount = File("$dataDir/interception_cnt").readText().toInt() - } catch (e: Exception) { - } - Log.i("Config initialized") - } - - private fun updateConfig(json: String) { - if (configStr == json) return - configStr = json - config = JsonConfig.fromJson(json) - synchronized(mLock) { - if (stopped) return - File("$dataDir/config.json").writeText(json) - } - if (!configCached) initConfig() - else Log.d("Update config: $config") - } - - private fun isUseHook(caller: String?, hookMethod: String): Boolean { - val appConfig = config.scope[caller] ?: return false - return appConfig.enableAllHooks || appConfig.applyHooks.contains(hookMethod) - } - - private fun isToHide(caller: String?, query: String?): Boolean { - if (caller == null || query == null) return false - if (caller in query) return false - val appConfig = config.scope[caller] ?: return false - if (appConfig.useWhitelist && appConfig.excludeSystemApps && query in systemApps) return false - - if (query in appConfig.extraAppList || query in appConfig.extraQueryParamRules) - return !appConfig.useWhitelist - for (tplName in appConfig.applyTemplates) { - val tpl = config.templates[tplName]!! - if (query in tpl.appList || query in tpl.queryParamRules) - return !appConfig.useWhitelist - } - - return appConfig.useWhitelist - } - - private fun removeList( - method: Method, - isParceled: Boolean, - hookMethod: String, - pkgNameObjList: List - ) { - allHooks.add(method.hookAfter { param -> - if (param.hasThrowable()) return@hookAfter - val caller = param.thisObject.getBinderCaller() - if (!isUseHook(caller, hookMethod)) return@hookAfter - - var isHidden = false - val list = - if (isParceled) param.result.invokeMethodAs>("getList")!! - else param.result as MutableList - val iterator = list.iterator() - val removed = mutableListOf() - - while (iterator.hasNext()) { - val pkg = getRecursiveField(iterator.next(), pkgNameObjList) as String? - if (isToHide(caller, pkg)) { - iterator.remove() - isHidden = true - if (config.detailLog) removed.add(pkg!!) - } - } - - if (isHidden) { - interceptionCount++ - Log.i("@Hide PMS caller: $caller method: ${param.method.name}") - Log.d("removeList $removed") - } - }) - } - - private fun setResult(method: Method, hookMethod: String, result: Any?) { - allHooks.add(method.hookAfter { param -> - val caller = param.thisObject.getBinderCaller() - if (!isUseHook(caller, hookMethod)) return@hookAfter - - if (isToHide(caller, param.args[0] as String?)) { - interceptionCount++ - param.result = result - Log.i("@Hide PMS caller: $caller method: ${param.method.name} param: ${param.args[0]}") - } - }) - } - - private fun resolveIntent(method: Method, hookMethod: String, result: Any?) { - allHooks.add(method.hookAfter { param -> - val caller = param.thisObject.getBinderCaller() - if (!isUseHook(caller, hookMethod)) return@hookAfter - - when (val it = param.args[0]) { - is Intent -> it.component?.packageName - is ComponentName -> it.packageName - else -> null - }?.let { - if (isToHide(caller, it)) { - interceptionCount++ - param.result = result - Log.i("@Hide PMS caller: $caller method: ${param.method.name} param: ${param.args[0]}") - return@hookAfter - } - } - }) - } - - /* Hijack getInstallerPackageName as communication service */ - object HMAService : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - val caller = param.thisObject.getBinderCaller() - var arg = param.args[0] as String? ?: return - - /* Non module calls require token validation */ - if (caller != hmaApp) { - if (!arg.startsWith(token)) { - if (!isUseHook(caller, "API requests")) return - if (isToHide(caller, arg)) { - interceptionCount++ - param.result = null - Log.i("@Hide PMS caller: $caller method: ${param.method.name} param: ${param.args[0]}") - } - return - } else arg = arg.removePrefix("$token#") - } - - when { - arg == "getServiceVersion" -> - param.result = BuildConfig.SERVICE_VERSION.toString() - - arg == "getExtensionVersion" -> - param.result = extensionVersion.toString() - - arg == "getServeTimes" -> - param.result = interceptionCount.toString() - - arg == "getLogs" -> - param.result = provideLogs() - - arg == "cleanLogs" -> { - synchronized(mLock) { logFile.apply { delete(); createNewFile() } } - param.result = "OK" - } - - arg.startsWith("addLog") -> { - addLog(arg.substring(7)) - param.result = "OK" - } - - arg.startsWith("submitConfig") -> { - updateConfig(arg.split("#")[1]) - param.result = "OK" - } - - arg.startsWith("stopSystemService") -> { - val split = arg.split("#") - stopService(split[1] == "true") - param.result = "OK" - } - } - } - } - - /* Remove all hooks */ - private fun stopService(cleanEnv: Boolean) { - stopped = true - File("$dataDir/tmp/ext_run").delete() - Log.i("Receive stop system service signal") - Log.i("Start to remove all hooks") - for (hook in allHooks) { - Log.i("Remove hook at ${hook.hookedMethod.name}") - hook.unhook() - } - Log.i("System service stopped") - synchronized(mLock) { - if (cleanEnv) { - Log.i("Clean runtime environment") - File(dataDir).deleteRecursively() - } - } - } - - private fun syncWithExtension() { - /* If extension not installed, make tmp by the service */ - File("$dataDir/tmp/ext_ver").apply { - if (exists()) { - var minApkVersion: Int - try { - val lines = readLines() - extensionVersion = lines[0].toInt() - minApkVersion = lines[1].toInt() - } catch (e: Exception) { - extensionVersion = 0 - minApkVersion = 0 - } - if (extensionVersion < BuildConfig.MIN_EXTENSION_VERSION) { - extensionVersion = -1 - File("$dataDir/tmp/ext_run").delete() - Log.e("Magisk extension version too old to work with the new system service") - } - if (BuildConfig.VERSION_CODE < minApkVersion) { - extensionVersion = -2 - File("$dataDir/tmp/ext_run").delete() - Log.e("System service version too old to work with the new Magisk extension") - } - delete() - } else File("$dataDir/tmp").apply { - deleteRecursively() - mkdirs() - } - } - } - - private fun searchDataDir() { - File("/data/misc/hide_my_applist").deleteRecursively() - File("/data/system").list()?.forEach { - if (it.startsWith("hide_my_applist")) { - if (this::dataDir.isInitialized) File("/data/system/$it").deleteRecursively() - else dataDir = "/data/system/$it" - } - } - if (!this::dataDir.isInitialized) { - dataDir = "/data/system/hide_my_applist_" + generateRandomString(16) - } - logFile = File("$dataDir/tmp/runtime.log") - } - - /* Load system service */ - fun entry() { - searchDataDir() - syncWithExtension() - generateToken() - try { - initConfig() - } catch (e: FileNotFoundException) { - Log.i("Config not cached, waiting for preference provider") - } catch (e: Exception) { - config = JsonConfig() - configStr = config.toString() - Log.e("Failed to read cached config, waiting for preference provider\n${e.stackTraceToString()}") - } - thread { - while (!stopped) { - if (configCached) synchronized(mLock) { - if (stopped) return@thread - File("$dataDir/interception_cnt").writeText(interceptionCount.toString()) - } - Thread.sleep(2000) - } - } - - val pms = loadClass("com.android.server.pm.PackageManagerService") - allHooks.addAll(pms.hookAllConstructorAfter { param -> - /* Cache system app list */ - val mSettings = param.thisObject.getObject("mSettings") - val mPackages = mSettings.getObjectAs>("mPackages") - for ((name, ps) in mPackages) { - if (ps != null && (ps.getObjectAs("pkgFlags") and ApplicationInfo.FLAG_SYSTEM != 0)) { - systemApps.add(name) - } - } - for (pkg in systemApps) - File("$dataDir/tmp/system_apps.list").appendText("$pkg\n") - - Log.i("System hook installed (Version ${BuildConfig.SERVICE_VERSION})") - Log.i("Data directory is at $dataDir") - }) - - /* ---Deal with 💩 ROMs--- */ - var extPms: Class<*>? = null - for (clazz in customPms) { - try { - extPms = loadClass(clazz) - break - } catch (e: ClassNotFoundException) { - } - } - val pmMethods = mutableSetOf() - val methodNames = mutableSetOf() - if (extPms != null) { - allHooks.addAll(extPms.hookAllConstructorAfter { param -> - Log.i("Non-AOSP PMS ${param.method.declaringClass.name}") - }) - for (method in extPms.declaredMethods) { - pmMethods.add(method) - methodNames.add(method.name) - } - } - /* ----------------------- */ - for (method in pms.declaredMethods) - if (method.name !in methodNames) - pmMethods.add(method) - - for (method in pmMethods) when (method.name) { - "getInstallerPackageName" - -> allHooks.add(XposedBridge.hookMethod(method, HMAService)) - - "getAllPackages" - -> removeList(method, false, "API requests", listOf()) - - "getInstalledPackages", - "getInstalledApplications", - "getPackagesHoldingPermissions", - "queryInstrumentation" - -> removeList(method, true, "API requests", listOf("packageName")) - - "getPackageInfo", - "getPackageGids", - "getApplicationInfo", - "getInstallSourceInfo", - "getLaunchIntentForPackage", - "getLeanbackLaunchIntentForPackage" - -> setResult(method, "API requests", null) - - "queryIntentActivities", - "queryIntentActivityOptions", - "queryIntentReceivers", - "queryIntentServices", - "queryIntentContentProviders" - -> removeList(method, true, "Intent queries", listOf("activityInfo", "packageName")) - - "getActivityInfo", - "resolveActivity", - "resolveActivityAsUser" - -> resolveIntent(method, "Intent queries", null) - - "getPackageUid" - -> setResult(method, "ID detections", -1) - - "getPackagesForUid" - -> allHooks.add(method.hookAfter { param -> - if (param.hasThrowable()) return@hookAfter - val caller = param.thisObject.getBinderCaller() - if (!isUseHook(caller, "ID detections")) return@hookAfter - if (param.result != null) { - var change = false - val list = mutableListOf() - val removed = mutableListOf() - for (str in param.result as Array) - if (isToHide(caller, str)) { - change = true - if (config.detailLog) removed.add(str) - } else list.add(str) - if (change) { - interceptionCount++ - param.result = list.toTypedArray() - Log.i("@Hide PMS caller: $caller method: ${param.method.name}") - Log.d("removeList $removed") - } - } - }) - } - } -} diff --git a/app/src/main/java/com/tsng/hidemyapplist/xposed/ServiceUtils.kt b/app/src/main/java/com/tsng/hidemyapplist/xposed/ServiceUtils.kt deleted file mode 100644 index 540e2bf8..00000000 --- a/app/src/main/java/com/tsng/hidemyapplist/xposed/ServiceUtils.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tsng.hidemyapplist.xposed - -import android.os.Binder -import com.github.kyuubiran.ezxhelper.utils.invokeMethodAutoAs -import de.robv.android.xposed.XposedHelpers - -object ServiceUtils { - @JvmStatic - fun getRecursiveField(entry: Any, list: List): Any? { - var field: Any? = entry - for (it in list) - field = XposedHelpers.getObjectField(field, it) ?: return null - return field - } - - @JvmStatic - fun Any.getBinderCaller(): String? { - return this.invokeMethodAutoAs("getNameForUid", Binder.getCallingUid()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tsng/hidemyapplist/xposed/XposedEntry.kt b/app/src/main/java/com/tsng/hidemyapplist/xposed/XposedEntry.kt deleted file mode 100644 index a18f32ab..00000000 --- a/app/src/main/java/com/tsng/hidemyapplist/xposed/XposedEntry.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tsng.hidemyapplist.xposed - -import com.github.kyuubiran.ezxhelper.init.EzXHelperInit -import com.github.kyuubiran.ezxhelper.init.InitFields.hostPackageName -import com.github.kyuubiran.ezxhelper.utils.getFieldByDesc -import com.tsng.hidemyapplist.BuildConfig -import de.robv.android.xposed.IXposedHookLoadPackage -import de.robv.android.xposed.IXposedHookZygoteInit -import de.robv.android.xposed.callbacks.XC_LoadPackage - -class XposedEntry : IXposedHookZygoteInit, IXposedHookLoadPackage { - override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) { - EzXHelperInit.initZygote(startupParam) - } - - override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { - if (lpparam.packageName == BuildConfig.APPLICATION_ID || lpparam.packageName == "android") { - EzXHelperInit.initHandleLoadPackage(lpparam) - EzXHelperInit.setLogTag("HMA Xposed") - EzXHelperInit.setToastTag("HMA") - if (hostPackageName == BuildConfig.APPLICATION_ID) - getFieldByDesc("Lcom/tsng/hidemyapplist/app/MyApplication;->isModuleActivated:Z").setBoolean(null, true) - else - PackageManagerService.entry() - } - } -} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3f61fd03..a1fd0af5 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -40,15 +40,6 @@ android:textAppearance="?attr/textAppearanceBody2" android:textColor="@android:color/white" /> - - - - - - - - - - - - - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_app_select.xml b/app/src/main/res/layout/fragment_app_select.xml index 9e68edb3..456c7b48 100644 --- a/app/src/main/res/layout/fragment_app_select.xml +++ b/app/src/main/res/layout/fragment_app_select.xml @@ -15,7 +15,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - - - \ No newline at end of file + + diff --git a/app/src/main/res/values-fr/arrays.xml b/app/src/main/res/values-fr/arrays.xml deleted file mode 100644 index b66de7c8..00000000 --- a/app/src/main/res/values-fr/arrays.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Reqêtes API - Requêtes intentionnelles - Détections d\’ID - Détections de fichiers - - diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 48bd1ab7..a0854a1b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -23,14 +23,6 @@ Nouvelle mise à jour: Nouvelles fonctionnalités - Extension Magisk installée - L\’extension Magisk n\’est pas installée - ❌ Version de l\’Extension [Magisk] trop ancienne - ❌ Version du service [Magisk] trop ancienne - - Installer l\’extension Magisk? - Install l\’extension Magisk par la racine. Pour plus d\’information, merci de vous référer à la page à propos. - Télécharger l\’application de test Nous avons développé une application individuelle de test et avons migré le test de détection dans celle-ci. Voulez-vous la télécharger maintenant? diff --git a/app/src/main/res/values-fr/strings_about.xml b/app/src/main/res/values-fr/strings_about.xml index d5d3cf1c..1e89c777 100644 --- a/app/src/main/res/values-fr/strings_about.xml +++ b/app/src/main/res/values-fr/strings_about.xml @@ -6,12 +6,9 @@ Comment utiliser ce module - Ai-je besoin de l\’extension magisk #\nSi vous avez besoin de la détection de fichier / interceptions de scannes de cartographies, vous devez installer l\’extension Magisk.\n\nAttention, l\’extension Magisk est un module Riru, qui requière Riru v25/26. - - #Comment activer le masquage #\nVous pouvez créer un modèle dans \"Gestionnaire de Modèles\". Appliquer ensuite le modèle dans \"Sélection Applications Éfficaces\". (Vous pouvez également sélectionner des applications Vous pouvez également sélectionner des applications supplémentaires pour les cibles.) Vous devriez et devriez UNIQUEMENT vérifier \"Sous Système\" dans le champ / liste blanche de module XPosed. - + #La modification du modèle est-elle efficace en temps réel#\nExcepté pour l\’interception de fichier et le scanne de cartographies, oui. Pour mettre à jour la politique d\’interception des fichiers, vous devez forcer l\’arrêt et le redémarrage des applications cibles. Différences entre les méthodes d\’accrochage diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 759bb867..10a6885e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -23,14 +23,6 @@ 新版本可用: 更新日志 - 安装 Magisk 插件? - 通过 root 安装插件,更多信息请查看关于页面。 - - Magisk 插件已安装 - Magisk 插件未安装 - ❌ [Magisk] 插件版本过旧 - ❌ [Magisk] 系统服务版本过旧 - 下载测试 app 我们开发了一个独立测试 app,并将检测测试迁移了过去。是否现在下载? diff --git a/app/src/main/res/values-zh-rCN/strings_about.xml b/app/src/main/res/values-zh-rCN/strings_about.xml index 569ee43b..8d240770 100644 --- a/app/src/main/res/values-zh-rCN/strings_about.xml +++ b/app/src/main/res/values-zh-rCN/strings_about.xml @@ -6,17 +6,14 @@ 如何使用该模块 - #我是否需要安装 magisk 插件#\n如果你需要文件拦截能力,你需要安装 magisk 插件。\n\n注意:magisk 插件是一个 Riru/Zygisk 模块,你需要安装 Riru v25/26或者Zygisk 才能工作。 - - #如何启用隐藏#\n在模板管理中可以创建拦截模板。在选择生效应用里对目标应用启用模板(或单独配置)。Xposed 模块作用域需要且*只需要*勾选“系统框架”。 - + #模板修改是实时生效的吗#\n除了文件检测拦截外,是的。更新文件拦截策略需要强制停止再重启目标 APP。 几种 hook 方式区别 - 正常 app 读取应用列表会使用 API requests 中的部分方法,“不安分”的 app 会使用其他几种方式(相当于漏洞)进行检测。\n\n一些例子\nAPI requests - 农行\nIntent queries - B站\nFile detections - 步道乐跑 + 正常 app 读取应用列表会使用 API requests 中的部分方法,“不安分”的 app 会使用其他几种方式(相当于漏洞)进行检测。\n\n一些例子\nAPI requests - 农行\nIntent queries - B站 开发者 支持和反馈 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8ef147ca..896d296d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -23,14 +23,6 @@ 新版本可用: 更新日誌 - 安裝 Magisk 模塊? - 透過 ROOT 安裝模塊,更多資訊請檢視關於頁面。 - - Magisk 模組已安裝 - Magisk 模組未安裝 - ❌ [Magisk] 模組版本過舊 - ❌ [Magisk] 系統服務版本過舊 - 下載測試 App 我們開發了一個獨立測試 App,並將檢測測試遷移了過去,是否現在下載? diff --git a/app/src/main/res/values-zh-rTW/strings_about.xml b/app/src/main/res/values-zh-rTW/strings_about.xml index 9736d785..4cea599e 100644 --- a/app/src/main/res/values-zh-rTW/strings_about.xml +++ b/app/src/main/res/values-zh-rTW/strings_about.xml @@ -6,17 +6,14 @@ 如何使用該模組 - #我是否需要安裝 Magisk 模塊#\n如果你需要檔案攔截能力,你需要安裝 Magisk 模塊。\n\n注意:Magisk 模塊是一個基於 Riru/Zygisk 的模塊,你需要安裝 Riru v25/26 或則 Zygisk 以上才能工作。 - - #如何啟用隱藏#\n在模板管理裡可以建立攔截模板。在選擇生效應用程式裡對目標應用程式啟用模板(或單獨設定)。Xposed 模組作用域需要且只需要勾選「系統框架」。 - + #模板修改是實時生效的嗎#\n除了文件檢測攔截外,是的。更新文件攔截策略需要強製停止再重啟目標應用程式。 幾種 Hook 方式區別 - 正常應用程式讀取應用程式列表會使用 API requests 中的部分方法,「不安分」的應用程式會使用其他幾種方式(相當於漏洞)進行檢測。\n\n一些例子\nAPI requests - 農行\nIntent queries - B站\nFile detections - 步道樂跑\n + 正常應用程式讀取應用程式列表會使用 API requests 中的部分方法,「不安分」的應用程式會使用其他幾種方式(相當於漏洞)進行檢測。\n\n一些例子\nAPI requests - 農行\nIntent queries - B站 開發者 支援和回饋 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 4a449035..432fd125 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -6,7 +6,6 @@ API requests Intent queries ID detections - File detections 256K @@ -18,4 +17,4 @@ 512 1024 - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ef1bc04..130a9648 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,14 +23,6 @@ New update: New features - Magisk extension installed - Magisk extension not installed - ❌ [Magisk] Extension version too old - ❌ [Magisk] System service version too old - - Install Magisk extension? - Install Magisk extension by root. For more information, please refer to the about page. - Download test app We have developed an individual detection test app and migrated detection test to it. Do you want to download it now? diff --git a/app/src/main/res/values/strings_about.xml b/app/src/main/res/values/strings_about.xml index a4febb65..39c1a1c3 100644 --- a/app/src/main/res/values/strings_about.xml +++ b/app/src/main/res/values/strings_about.xml @@ -6,12 +6,9 @@ How to use this module - #Do I need magisk extension#\nIf you need file detection interceptions, you need install the magisk extension.\n\nAttention, the magisk extension is a Riru/Zygisk module, which requires Riru v25/26 or Zygisk. - - #How to enable hide#\nYou can create templates in \"Manage Templates\". Then apply the templates in \"Select Effective Apps\". (You can also select extra apps for targets.) You should and ONLY should check \"System Framework\" in Xposed module scope / whitelist. - + #Is template modification effective in real time#\nExcept for file interception, yes. To update file interception policy, you need to force stop and restart target apps. Differences among hook methods @@ -21,4 +18,4 @@ Developer Support and feedback Open source licenses - \ No newline at end of file + diff --git a/build.gradle.kts b/build.gradle.kts index b5a2b115..d0000414 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,14 +1,25 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.gradle.BaseExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.konan.properties.Properties + buildscript { repositories { google() mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:7.1.1") - classpath("com.google.gms:google-services:4.3.10") + classpath("com.android.tools.build:gradle:7.2.1") + classpath("com.google.gms:google-services:4.3.12") } } +plugins { + id("com.android.application") apply false + id("com.android.library") apply false + kotlin("android") apply false +} + fun String.execute(currentWorkingDir: File = file("./")): String { val byteOut = java.io.ByteArrayOutputStream() project.exec { @@ -20,17 +31,82 @@ fun String.execute(currentWorkingDir: File = file("./")): String { } val minSdkVer by extra(24) -val targetSdkVer by extra(32) +val targetSdkVer by extra(33) -val appVerName by extra("2.3.3") -val appVerCode by extra(74) -val serviceVer by extra(74) -val minExtensionVer by extra(35) +val appVerName by extra("3.0.0") +val appVerCode by extra(75) +val serviceVer by extra(75) val minBackupVer by extra(65) +val androidSourceCompatibility = JavaVersion.VERSION_11 +val androidTargetCompatibility = JavaVersion.VERSION_11 + val gitCommitCount by extra("git rev-list HEAD --count".execute()) val gitCommitHash by extra("git rev-parse --verify --short HEAD".execute()) +val localProperties = Properties() +localProperties.load(file("local.properties").inputStream()) + tasks.register("clean", Delete::class) { delete(rootProject.buildDir) } + +fun Project.configureBaseExtension() { + extensions.findByType()?.run { + compileSdkVersion(targetSdkVer) + + defaultConfig { + minSdk = minSdkVer + targetSdk = targetSdkVer + versionCode = appVerCode + versionName = appVerName + if (localProperties.getProperty("buildWithGitSuffix").toBoolean()) + versionNameSuffix = ".r${gitCommitCount}.${gitCommitHash}" + } + + val signingCfg = signingConfigs.create("config") { + storeFile = rootProject.file(localProperties.getProperty("fileDir")) + storePassword = localProperties.getProperty("storePassword") + keyAlias = localProperties.getProperty("keyAlias") + keyPassword = localProperties.getProperty("keyPassword") + } + + buildTypes { + all { + signingConfig = signingCfg + } + named("release") { + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = androidSourceCompatibility + targetCompatibility = androidTargetCompatibility + } + } + + extensions.findByType()?.run { + buildTypes { + named("release") { + isShrinkResources = true + } + } + } + + extensions.findByType()?.run { + kotlinOptions { + jvmTarget = "11" + } + } +} + +subprojects { + plugins.withId("com.android.application") { + configureBaseExtension() + } + plugins.withId("com.android.library") { + configureBaseExtension() + } +} diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 00000000..c1de5655 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.serialization") +} + +val serviceVer: Int by rootProject.extra +val minExtensionVer: Int by rootProject.extra +val minBackupVer: Int by rootProject.extra + +android { + namespace = "icu.nullptr.hidemyapplist.common" + + defaultConfig { + buildConfigField("int", "SERVICE_VERSION", serviceVer.toString()) + buildConfigField("int", "MIN_BACKUP_VERSION", minBackupVer.toString()) + } +} + +dependencies { + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3") +} diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro new file mode 100644 index 00000000..54b76082 --- /dev/null +++ b/common/proguard-rules.pro @@ -0,0 +1,2 @@ +-keep class icu.nullptr.hidemyapplist.common.JsonConfig { *; } +-keep class icu.nullptr.hidemyapplist.common.JsonConfig$* { *; } diff --git a/common/src/main/aidl/icu/nullptr/hidemyapplist/common/IHMAService.aidl b/common/src/main/aidl/icu/nullptr/hidemyapplist/common/IHMAService.aidl new file mode 100644 index 00000000..bac8db98 --- /dev/null +++ b/common/src/main/aidl/icu/nullptr/hidemyapplist/common/IHMAService.aidl @@ -0,0 +1,18 @@ +package icu.nullptr.hidemyapplist.common; + +interface IHMAService { + + void stopService(boolean cleanEnv) = 0; + + void syncConfig(String json) = 1; + + int getServiceVersion() = 2; + + int getFilterCount() = 3; + + String getLogs() = 4; + + void sendLog(int level, String tag, String msg) = 5; + + void clearLogs() = 6; +} diff --git a/common/src/main/java/icu/nullptr/hidemyapplist/common/Constants.java b/common/src/main/java/icu/nullptr/hidemyapplist/common/Constants.java new file mode 100644 index 00000000..ddb7f961 --- /dev/null +++ b/common/src/main/java/icu/nullptr/hidemyapplist/common/Constants.java @@ -0,0 +1,15 @@ +package icu.nullptr.hidemyapplist.common; + +public class Constants { + public static final String APP_PACKAGE_NAME = "com.tsng.hidemyapplist"; + public static final String CLASS_PMS = "com.android.server.pm.PackageManagerService"; + public static final String[] CLASS_EXT_PMS = { + "com.android.server.pm.OplusPackageManagerService", + "com.android.server.pm.OppoPackageManagerService" + }; + + public static final String DESCRIPTOR = "android.content.pm.IPackageManager"; + public static final int TRANSACTION = 'H' << 24 | 'M' << 16 | 'A' << 8 | 'D'; + public static final int ACTION_GET_BINDER = 1; + public static final int ACTION_SEND_LOG = 2; +} diff --git a/app/src/main/java/com/tsng/hidemyapplist/JsonConfig.kt b/common/src/main/java/icu/nullptr/hidemyapplist/common/JsonConfig.kt similarity index 52% rename from app/src/main/java/com/tsng/hidemyapplist/JsonConfig.kt rename to common/src/main/java/icu/nullptr/hidemyapplist/common/JsonConfig.kt index 6c5ea33b..b8ac6824 100644 --- a/app/src/main/java/com/tsng/hidemyapplist/JsonConfig.kt +++ b/common/src/main/java/icu/nullptr/hidemyapplist/common/JsonConfig.kt @@ -1,15 +1,24 @@ -package com.tsng.hidemyapplist +package icu.nullptr.hidemyapplist.common -import com.google.gson.Gson +import kotlinx.serialization.Serializable -class JsonConfig { +@Serializable +data class JsonConfig( + var configVersion: Int = BuildConfig.SERVICE_VERSION, + var detailLog: Boolean = false, + var maxLogSize: Int = 512, + val templates: MutableMap = mutableMapOf(), + val scope: MutableMap = mutableMapOf() +) { + @Serializable data class Template( - val isWhitelist: Boolean, + val isWhitelist: Boolean = false, val appList: MutableSet = mutableSetOf(), val mapsRules: MutableSet = mutableSetOf(), val queryParamRules: MutableSet = mutableSetOf() ) + @Serializable data class AppConfig( var useWhitelist: Boolean = false, var enableAllHooks: Boolean = false, @@ -20,26 +29,4 @@ class JsonConfig { val extraMapsRules: MutableSet = mutableSetOf(), val extraQueryParamRules: MutableSet = mutableSetOf() ) - - var configVersion = BuildConfig.VERSION_CODE - var detailLog = false - var maxLogSize = 512 - val templates = mutableMapOf() - val scope = mutableMapOf() - - override fun toString(): String { - return Gson().toJson(this) - } - - fun clear() { - templates.clear() - scope.clear() - } - - companion object { - @JvmStatic - fun fromJson(str: String): JsonConfig { - return Gson().fromJson(str, JsonConfig::class.java) - } - } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 9a8a6da0..f1a4ddc0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,21 +1,8 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app"s APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true +android.experimental.enableNewResourceShrinker=true +android.experimental.enableNewResourceShrinker.preciseShrinking=true android.enableAppCompileTimeRClass=true android.nonTransitiveRClass=true +android.enableR8.fullMode=true +android.useAndroidX=true + +agpVersion=7.2.1 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 88699608..cade3fae 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Sun Feb 20 21:00:09 CST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index 72ae5a8f..5e1d3632 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,27 +1,35 @@ +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() - maven("https://jcenter.bintray.com") maven("https://jitpack.io") maven("https://api.xposed.info/") } } pluginManagement { + val agpVersion: String by settings repositories { gradlePluginPortal() google() mavenCentral() } plugins { - id("com.android.application").version("7.1.1") - id("com.android.library").version("7.1.1") - id("org.jetbrains.kotlin.android").version("1.6.10") + id("com.android.application") version agpVersion + id("com.android.library") version agpVersion + id("dev.rikka.tools.refine") version "3.1.1" + id("org.jetbrains.kotlin.android") version "1.7.0" + kotlin("plugin.serialization") version "1.7.0" } } -rootProject.name = "Hide My Applist" +rootProject.name = "HideMyApplist" -include(":app") +include( + ":app", + ":common", + ":xposed" +) diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts new file mode 100644 index 00000000..3969e70f --- /dev/null +++ b/xposed/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("com.android.library") + id("dev.rikka.tools.refine") + kotlin("android") +} + +android { + namespace = "icu.nullptr.hidemyapplist.xposed" + + buildFeatures { + buildConfig = false + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(projects.common) + + implementation("com.github.kyuubiran:EzXHelper:0.9.7") + implementation("dev.rikka.hidden:compat:2.3.1") + compileOnly("de.robv.android.xposed:api:82") + compileOnly("dev.rikka.hidden:stub:2.3.1") +} diff --git a/xposed/proguard-rules.pro b/xposed/proguard-rules.pro new file mode 100644 index 00000000..1ddb1092 --- /dev/null +++ b/xposed/proguard-rules.pro @@ -0,0 +1 @@ +-keep class icu.nullptr.hidemyapplist.xposed.XposedEntry diff --git a/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/BridgeService.kt b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/BridgeService.kt new file mode 100644 index 00000000..e5c09863 --- /dev/null +++ b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/BridgeService.kt @@ -0,0 +1,91 @@ +package icu.nullptr.hidemyapplist.xposed + +import android.content.pm.IPackageManager +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.os.Parcel +import android.util.Log +import com.github.kyuubiran.ezxhelper.utils.* +import icu.nullptr.hidemyapplist.common.Constants + +object BridgeService { + + private const val TAG = "HMA-Bridge" + + @JvmStatic + private lateinit var service: HMAService + private var appUid = 0 + + fun start(pms: IPackageManager) { + service = HMAService(pms) + appUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + service.pms.getPackageUid(Constants.APP_PACKAGE_NAME, 0L, 0) + } else { + service.pms.getPackageUid(Constants.APP_PACKAGE_NAME, 0, 0) + } + doHooks() + } + + private fun doHooks() { + Binder::class.java.method("execTransact").hookBefore { param -> + val code = param.args[0] as Int + val dataObj = param.args[1] as Long + val replyObj = param.args[2] as Long + val flags = param.args[3] as Int + if (code == Constants.TRANSACTION) { + param.result = execTransact(code, dataObj, replyObj, flags) + } + } + } + + private fun execTransact(code: Int, dataObj: Long, replyObj: Long, flags: Int): Boolean { + val fromNativePointerMethod by lazy { + Parcel::class.java.method("obtain", argTypes = argTypes(Long::class.java)) + } + + val data = fromNativePointerMethod.invoke(null, dataObj) as Parcel? ?: return false + val reply = fromNativePointerMethod.invoke(null, replyObj) as Parcel? + + val res = try { + onTransact(code, data, reply, flags) + } catch (e: Exception) { + if (flags and IBinder.FLAG_ONEWAY != 0) { + Log.w(TAG, "Caught an exception from the binder stub implementation.") + } else if (reply != null) { + reply.setDataPosition(0) + reply.writeException(e) + } + false + } finally { + data.setDataPosition(0) + reply?.setDataPosition(0) + } + if (res) { + data.recycle() + reply?.recycle() + } + return res + } + + private fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + data.enforceInterface(Constants.DESCRIPTOR) + return when (data.readInt()) { + Constants.ACTION_GET_BINDER -> { + if (Binder.getCallingUid() == appUid && service.hooksInstalled) { + reply?.writeNoException() + reply?.writeStrongBinder(service.asBinder()) + true + } else { + Log.w(TAG, "Invalid connection") + false + } + } + Constants.ACTION_SEND_LOG -> { + service.sendLog(data.readInt(), data.readString()!!, data.readString()!!) + false + } + else -> false + } + } +} diff --git a/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMAService.kt b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMAService.kt new file mode 100644 index 00000000..be6b6e9d --- /dev/null +++ b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMAService.kt @@ -0,0 +1,296 @@ +package icu.nullptr.hidemyapplist.xposed + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.IPackageManager +import android.util.Log +import com.github.kyuubiran.ezxhelper.utils.* +import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XposedBridge +import icu.nullptr.hidemyapplist.common.Constants +import icu.nullptr.hidemyapplist.common.JsonConfig +import icu.nullptr.hidemyapplist.common.BuildConfig +import icu.nullptr.hidemyapplist.common.IHMAService +import icu.nullptr.hidemyapplist.xposed.Utils.getBinderCaller +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import java.io.File +import java.lang.reflect.Method +import java.text.SimpleDateFormat +import java.util.* + +private const val TAG = "HMA-Service" + +class HMAService(val pms: IPackageManager) : IHMAService.Stub() { + + var hooksInstalled = false + + private lateinit var dataDir: String + private lateinit var logFile: File + private lateinit var oldLogFile: File + + private val configLock = Any() + private val loggerLock = Any() + private val systemApps = mutableSetOf() + private val allHooks = mutableSetOf() + private fun XC_MethodHook.Unhook.yes() { allHooks.add(this) } + + private var config = JsonConfig() + private var filterCount = 0 + set(value) { + field = value + if (field % 100 == 0) { + synchronized(configLock) { + File("$dataDir/filter_count").writeText(field.toString()) + } + } + } + + init { + searchDataDir() + loadConfig() + installHooks() + } + + fun logD(tag: String, msg: String) = sendLog(Log.DEBUG, tag, msg) + fun logI(tag: String, msg: String) = sendLog(Log.INFO, tag, msg) + fun logW(tag: String, msg: String) = sendLog(Log.WARN, tag, msg) + fun logE(tag: String, msg: String) = sendLog(Log.ERROR, tag, msg) + + private fun searchDataDir() { + File("/data/misc/hide_my_applist").deleteRecursively() + File("/data/system").list()?.forEach { + if (it.startsWith("hide_my_applist")) { + if (this::dataDir.isInitialized) File("/data/system/$it").deleteRecursively() + else dataDir = "/data/system/$it" + } + } + if (!this::dataDir.isInitialized) { + dataDir = "/data/system/hide_my_applist_" + Utils.generateRandomString(16) + } + logFile = File("$dataDir/tmp/runtime.log") + oldLogFile = File("$dataDir/tmp/old.log") + } + + private fun loadConfig() { + filterCount = File("$dataDir/filter_count").readText().toInt() + val json = File("$dataDir/config.json").readText() + val loading = runCatching { + Json.decodeFromString(json) + }.getOrElse { + logE(TAG, "Failed to parse config.json") + return + } + if (loading.configVersion != BuildConfig.SERVICE_VERSION) { + logW(TAG, "Config version mismatch, need to reload") + return + } + config = loading + logI(TAG, "Config loaded") + } + + private fun installHooks() { + val mSettings = pms.findFieldObject(findSuper = true) { name == "mSettings" } + val mPackages = mSettings.getObjectAs>("mPackages") + for ((name, ps) in mPackages) { + if (ps != null && (ps.getObjectAs("pkgFlags") and ApplicationInfo.FLAG_SYSTEM != 0)) { + systemApps.add(name) + } + } + + val pmMethods = buildSet { + addAll(pms::class.java.declaredMethods) + val pmsClass = loadClass(Constants.CLASS_PMS) + if (pms::class.java != pmsClass) { // Has custom pms + val added = mapTo(mutableSetOf()) { it.name } + pmsClass.declaredMethods.forEach { + if (!added.contains(it.name)) add(it) + } + } + } + for (method in pmMethods) when (method.name) { + "getAllPackages" + -> removeList(method, false, "API requests", listOf()).yes() + + "getInstalledPackages", + "getInstalledApplications", + "getPackagesHoldingPermissions", + "queryInstrumentation" + -> removeList(method, true, "API requests", listOf("packageName")).yes() + + "getPackageInfo", + "getPackageGids", + "getApplicationInfo", + "getInstallSourceInfo", + "getInstallerPackageName", + "getLaunchIntentForPackage", + "getLeanbackLaunchIntentForPackage" + -> setResult(method, "API requests", null).yes() + + "queryIntentActivities", + "queryIntentActivityOptions", + "queryIntentReceivers", + "queryIntentServices", + "queryIntentContentProviders" + -> removeList(method, true, "Intent queries", listOf("activityInfo", "packageName")).yes() + + "getActivityInfo", + "resolveActivity", + "resolveActivityAsUser" + -> resolveIntent(method, "Intent queries", null).yes() + + "getPackageUid" + -> setResult(method, "ID detections", -1).yes() + } + + hooksInstalled = true + logI(TAG, "Hooks installed") + } + + private fun shouldHook(caller: String?, hookMethod: String): Boolean { + val appConfig = config.scope[caller] ?: return false + return appConfig.enableAllHooks || appConfig.applyHooks.contains(hookMethod) + } + + private fun shouldHide(caller: String?, query: String?): Boolean { + if (caller == null || query == null) return false + if (caller in query) return false + val appConfig = config.scope[caller] ?: return false + if (appConfig.useWhitelist && appConfig.excludeSystemApps && query in systemApps) return false + + if (query in appConfig.extraAppList || query in appConfig.extraQueryParamRules) { + return !appConfig.useWhitelist + } + for (tplName in appConfig.applyTemplates) { + val tpl = config.templates[tplName]!! + if (query in tpl.appList || query in tpl.queryParamRules) + return !appConfig.useWhitelist + } + + return appConfig.useWhitelist + } + + private fun removeList( + method: Method, + isParceled: Boolean, + hookMethod: String, + pkgNameObjList: List + ) = method.hookAfter { param -> + if (param.hasThrowable()) return@hookAfter + val caller = param.thisObject.getBinderCaller() + if (!shouldHook(caller, hookMethod)) return@hookAfter + + var isHidden = false + val list = + if (isParceled) param.result.invokeMethodAs>("getList")!! + else param.result as MutableList + val iterator = list.iterator() + val removed = mutableListOf() + + while (iterator.hasNext()) { + val pkg = Utils.getRecursiveField(iterator.next(), pkgNameObjList) as String? + if (shouldHide(caller, pkg)) { + iterator.remove() + isHidden = true + if (config.detailLog) removed.add(pkg!!) + } + } + + if (isHidden) { + filterCount++ + logI(TAG, "@Hide PMS caller: $caller method: ${param.method.name}") + logD(TAG, "RemoveList $removed") + } + } + + private fun setResult( + method: Method, + hookMethod: String, + result: Any? + ) = method.hookAfter { param -> + val caller = param.thisObject.getBinderCaller() + if (!shouldHook(caller, hookMethod)) return@hookAfter + + if (shouldHide(caller, param.args[0] as String?)) { + filterCount++ + param.result = result + logI(TAG, "@Hide PMS caller: $caller method: ${param.method.name} param: ${param.args[0]}") + } + } + + private fun resolveIntent( + method: Method, + hookMethod: String, + result: Any? + ) = method.hookAfter { param -> + val caller = param.thisObject.getBinderCaller() + if (!shouldHook(caller, hookMethod)) return@hookAfter + + when (val it = param.args[0]) { + is Intent -> listOf(it.action, it.component?.packageName) + is ComponentName -> listOf(it.packageName, it.className) + else -> emptyList() + }.forEach { + if (shouldHide(caller, it)) { + filterCount++ + param.result = result + logI(TAG, "@Hide PMS caller: $caller method: ${param.method.name} param: ${param.args[0]}") + return@hookAfter + } + } + } + + override fun stopService(cleanEnv: Boolean) { + synchronized(configLock) { + allHooks.forEach(XC_MethodHook.Unhook::unhook) + allHooks.clear() + hooksInstalled = false + } + logI(TAG, "Hooks cleared") + } + + override fun syncConfig(json: String) { + synchronized(configLock) { + File("$dataDir/config.json").writeText(json) + val newConfig = Json.decodeFromString(json) + if (newConfig.configVersion != BuildConfig.SERVICE_VERSION) { + logW(TAG, "Sync config: version mismatch, need reboot") + return + } + } + logI(TAG, "Config synced") + } + + override fun getServiceVersion() = BuildConfig.SERVICE_VERSION + + override fun getFilterCount() = filterCount + + override fun getLogs() = synchronized(loggerLock) { logFile.readText() } + + override fun sendLog(level: Int, tag: String, msg: String) { + if (level <= Log.DEBUG && !config.detailLog) return + val levelStr = when (level) { + Log.DEBUG -> "DEBUG" + Log.INFO -> " INFO" + Log.WARN -> " WARN" + Log.ERROR -> "ERROR" + else -> "?????" + } + val date = SimpleDateFormat("MM-dd HH:mm:ss", Locale.getDefault()).format(Date()) + var fmt = "[$levelStr] $date ($tag) $msg" + if (!msg.endsWith('\n')) fmt += '\n' + synchronized(loggerLock) { + if (logFile.length() / 1024 > config.maxLogSize) clearLogs() + XposedBridge.log(fmt) + logFile.appendText(fmt) + } + } + + override fun clearLogs() { + synchronized(loggerLock) { + oldLogFile.delete() + logFile.renameTo(oldLogFile) + } + } +} diff --git a/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/Utils.kt b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/Utils.kt new file mode 100644 index 00000000..875dea41 --- /dev/null +++ b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/Utils.kt @@ -0,0 +1,32 @@ +package icu.nullptr.hidemyapplist.xposed + +import android.os.Binder +import com.github.kyuubiran.ezxhelper.utils.invokeMethodAutoAs +import de.robv.android.xposed.XposedHelpers +import java.util.* + +object Utils { + + fun generateRandomString(length: Int): String { + val leftLimit = 97 // letter 'a' + val rightLimit = 122 // letter 'z' + val random = Random() + val buffer = StringBuilder(length) + for (i in 0 until length) { + val randomLimitedInt = leftLimit + (random.nextFloat() * (rightLimit - leftLimit + 1)).toInt() + buffer.append(randomLimitedInt.toChar()) + } + return buffer.toString() + } + + fun getRecursiveField(entry: Any, list: List): Any? { + var field: Any? = entry + for (it in list) + field = XposedHelpers.getObjectField(field, it) ?: return null + return field + } + + fun Any.getBinderCaller(): String? { + return this.invokeMethodAutoAs("getNameForUid", Binder.getCallingUid()) + } +} diff --git a/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/XposedEntry.kt b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/XposedEntry.kt new file mode 100644 index 00000000..033c5554 --- /dev/null +++ b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/XposedEntry.kt @@ -0,0 +1,61 @@ +package icu.nullptr.hidemyapplist.xposed + +import android.content.pm.IPackageManager +import android.os.ServiceManager +import android.util.Log +import com.github.kyuubiran.ezxhelper.init.EzXHelperInit +import com.github.kyuubiran.ezxhelper.utils.hookAllConstructorAfter +import com.github.kyuubiran.ezxhelper.utils.loadClass +import de.robv.android.xposed.IXposedHookLoadPackage +import de.robv.android.xposed.IXposedHookZygoteInit +import de.robv.android.xposed.callbacks.XC_LoadPackage +import icu.nullptr.hidemyapplist.common.Constants +import kotlin.concurrent.thread + +private const val TAG = "HMA-XposedEntry" + +private fun waitSystemService(name: String) { + while (ServiceManager.getService(name) == null) { + try { + Log.i(TAG, "service $name is not started, wait 1s.") + Thread.sleep(1000) + } catch (e: InterruptedException) { + Log.i(TAG, Log.getStackTraceString(e)) + } + } +} + +class XposedEntry : IXposedHookZygoteInit, IXposedHookLoadPackage { + override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) { + EzXHelperInit.initZygote(startupParam) + } + + override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { + if (lpparam.packageName == "android") { + EzXHelperInit.initHandleLoadPackage(lpparam) + + var pms: IPackageManager? = null + val pmsClass = loadClass(Constants.CLASS_PMS) + pmsClass.hookAllConstructorAfter { param -> + pms = param.thisObject as IPackageManager + Log.d(TAG, "Got pms: $pms") + } + Constants.CLASS_EXT_PMS.forEach { + runCatching { + hookAllConstructorAfter(it) { param -> + pms = param.thisObject as IPackageManager + Log.d(TAG, "Got custom pms: $pms") + } + } + } + thread { + waitSystemService("package") + if (pms != null) { + BridgeService.start(pms!!) + } else { + Log.e(TAG, "Package service started, but instance is not captured") + } + } + } + } +}