From c763b054f329313ad485d63e3f463a973b46c751 Mon Sep 17 00:00:00 2001 From: frknkrc44 Date: Wed, 27 Aug 2025 01:24:46 +0300 Subject: [PATCH] Add "Settings Presets" --- .../hidemyapplist/service/ServiceClient.kt | 5 +- .../ui/fragment/AppSettingsFragment.kt | 41 +++++ app/src/main/res/values/strings.xml | 5 + app/src/main/res/xml/app_settings.xml | 4 + build.gradle.kts | 2 +- .../hidemyapplist/common/JsonConfig.kt | 1 + .../hidemyapplist/common/SettingsPresets.kt | 31 ++++ .../settings_presets/AccessibilityPreset.kt | 20 +++ .../common/settings_presets/BasePreset.kt | 7 + .../DeveloperOptionsPreset.kt | 20 +++ .../settings_presets/ReplacementItem.kt | 11 ++ .../hidemyapplist/xposed/HMAService.kt | 7 + .../xposed/HMASettingsProviderService.kt | 148 ++++++++++++++++++ .../hidemyapplist/xposed/XposedEntry.kt | 2 + .../xposed/hook/AccessibilityHook.kt | 51 ++++++ 15 files changed, 351 insertions(+), 4 deletions(-) create mode 100644 common/src/main/java/icu/nullptr/hidemyapplist/common/SettingsPresets.kt create mode 100644 common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/AccessibilityPreset.kt create mode 100644 common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/BasePreset.kt create mode 100644 common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/DeveloperOptionsPreset.kt create mode 100644 common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/ReplacementItem.kt create mode 100644 xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMASettingsProviderService.kt create mode 100644 xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/hook/AccessibilityHook.kt diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/service/ServiceClient.kt b/app/src/main/java/icu/nullptr/hidemyapplist/service/ServiceClient.kt index adb70f8..18e735e 100644 --- a/app/src/main/java/icu/nullptr/hidemyapplist/service/ServiceClient.kt +++ b/app/src/main/java/icu/nullptr/hidemyapplist/service/ServiceClient.kt @@ -1,7 +1,6 @@ package icu.nullptr.hidemyapplist.service import android.os.IBinder -import android.os.IBinder.DeathRecipient import android.os.Parcel import android.os.RemoteException import android.os.ServiceManager @@ -12,7 +11,7 @@ import java.lang.reflect.InvocationHandler import java.lang.reflect.Method import java.lang.reflect.Proxy -object ServiceClient : IHMAService, DeathRecipient { +object ServiceClient : IHMAService, IBinder.DeathRecipient { private const val TAG = "ServiceClient" @@ -96,4 +95,4 @@ object ServiceClient : IHMAService, DeathRecipient { override fun stopService(cleanEnv: Boolean) { getServiceLegacy()?.stopService(cleanEnv) } -} +} \ No newline at end of file diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppSettingsFragment.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppSettingsFragment.kt index c6ec812..e6ac92f 100644 --- a/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppSettingsFragment.kt +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppSettingsFragment.kt @@ -18,6 +18,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.dialog.MaterialAlertDialogBuilder import icu.nullptr.hidemyapplist.common.AppPresets import icu.nullptr.hidemyapplist.common.JsonConfig +import icu.nullptr.hidemyapplist.common.SettingsPresets import icu.nullptr.hidemyapplist.service.ConfigManager import icu.nullptr.hidemyapplist.ui.util.navController import icu.nullptr.hidemyapplist.ui.util.setupToolbar @@ -110,6 +111,11 @@ class AppSettingsFragment : Fragment(R.layout.fragment_settings) { getString(R.string.app_preset_using, pack.config.applyPresets.size) } + private fun updateApplySettingsPresets() { + findPreference("applySettingsPresets")?.title = + getString(R.string.app_settings_preset_using, pack.config.applySettingsPresets.size) + } + private fun updateExtraAppList(useWhiteList: Boolean) { findPreference("extraAppList")?.title = if (useWhiteList) getString(R.string.app_extra_apps_visible_count, pack.config.extraAppList.size) @@ -187,6 +193,40 @@ class AppSettingsFragment : Fragment(R.layout.fragment_settings) { .show() true } + findPreference("applySettingsPresets")?.setOnPreferenceClickListener { + val presetNames = SettingsPresets.instance.getAllPresetNames() + val presetTranslations = presetNames.map { name -> + try { + val id = resources.getIdentifier( + "settings_preset_${name}", + "string", + BuildConfig.APPLICATION_ID + ) + + return@map if (id != 0) { getString(id) } else { name } + } catch (ignore: Throwable) {} + + name + } + + val presets = presetNames.zip(presetTranslations).toMap().toSortedMap() + val checked = presets.keys.map { + pack.config.applySettingsPresets.contains(it) + }.toBooleanArray() + val presetValues = presets.values.toTypedArray() + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.app_choose_preset) + .setMultiChoiceItems(presetValues, checked) { _, i, value -> checked[i] = value } + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + pack.config.applySettingsPresets = presetValues.mapIndexedNotNullTo(mutableSetOf()) { i, name -> + if (checked[i]) presets.filterValues { v -> v == name }.keys.first() else null + } + updateApplySettingsPresets() + } + .show() + true + } findPreference("extraAppList")?.setOnPreferenceClickListener { parent.setFragmentResultListener("app_select") { _, bundle -> pack.config.extraAppList = bundle.getStringArrayList("checked")!!.toMutableSet() @@ -203,6 +243,7 @@ class AppSettingsFragment : Fragment(R.layout.fragment_settings) { } updateApplyTemplates() updateApplyPresets() + updateApplySettingsPresets() updateExtraAppList(pack.config.useWhitelist) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d1231a..f6cbc40 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,6 +88,7 @@ Turn off this option only when really necessary. It is likely to cause crash with it disabled Using %d templates Using %d presets + Using %d settings presets %d additional apps visible %d additional apps invisible @@ -96,6 +97,10 @@ Root managers/Rooted apps Custom ROM apps + + Accessibility + Developer options + Save logs Clear logs diff --git a/app/src/main/res/xml/app_settings.xml b/app/src/main/res/xml/app_settings.xml index 86f0f1a..96af57e 100644 --- a/app/src/main/res/xml/app_settings.xml +++ b/app/src/main/res/xml/app_settings.xml @@ -38,6 +38,10 @@ android:icon="@drawable/outline_assignment_24" android:key="applyPresets" tools:title="@string/app_preset_using" /> + = mutableSetOf(), var applyPresets: MutableSet = mutableSetOf(), + var applySettingsPresets: MutableSet = mutableSetOf(), var extraAppList: MutableSet = mutableSetOf() ) { override fun toString() = encoder.encodeToString(this) diff --git a/common/src/main/java/icu/nullptr/hidemyapplist/common/SettingsPresets.kt b/common/src/main/java/icu/nullptr/hidemyapplist/common/SettingsPresets.kt new file mode 100644 index 0000000..42b8529 --- /dev/null +++ b/common/src/main/java/icu/nullptr/hidemyapplist/common/SettingsPresets.kt @@ -0,0 +1,31 @@ +package icu.nullptr.hidemyapplist.common + +import icu.nullptr.hidemyapplist.common.settings_presets.AccessibilityPreset +import icu.nullptr.hidemyapplist.common.settings_presets.BasePreset +import icu.nullptr.hidemyapplist.common.settings_presets.DeveloperOptionsPreset + +class SettingsPresets private constructor() { + private val presetList = mutableListOf() + + companion object { + private var hiddenInstance: SettingsPresets? = null + + val instance: SettingsPresets + get() { + if (hiddenInstance == null) { + hiddenInstance = SettingsPresets() + } + + return hiddenInstance!! + } + } + + fun getAllPresetNames() = presetList.map { it.name }.toTypedArray() + // fun filterPresetsByName(names: Array) = presetList.filter { names.contains(it.name) } + fun getPresetByName(name: String) = presetList.firstOrNull { it.name == name } + + init { + presetList.add(DeveloperOptionsPreset()) + presetList.add(AccessibilityPreset()) + } +} diff --git a/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/AccessibilityPreset.kt b/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/AccessibilityPreset.kt new file mode 100644 index 0000000..678a2d2 --- /dev/null +++ b/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/AccessibilityPreset.kt @@ -0,0 +1,20 @@ +package icu.nullptr.hidemyapplist.common.settings_presets + +import android.provider.Settings + +class AccessibilityPreset : BasePreset(NAME) { + companion object { + const val NAME = "accessibility" + } + + override val settingsKVPairs = listOf( + ReplacementItem( + name = Settings.Secure.ACCESSIBILITY_ENABLED, + value = "0", + ), + ReplacementItem( + name = Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + value = null, + ), + ) +} diff --git a/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/BasePreset.kt b/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/BasePreset.kt new file mode 100644 index 0000000..67ef420 --- /dev/null +++ b/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/BasePreset.kt @@ -0,0 +1,7 @@ +package icu.nullptr.hidemyapplist.common.settings_presets + +abstract class BasePreset(val name: String) { + protected abstract val settingsKVPairs: List + + fun getSpoofedValue(key: String) = settingsKVPairs.firstOrNull { it.name == key } +} diff --git a/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/DeveloperOptionsPreset.kt b/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/DeveloperOptionsPreset.kt new file mode 100644 index 0000000..c96ae3c --- /dev/null +++ b/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/DeveloperOptionsPreset.kt @@ -0,0 +1,20 @@ +package icu.nullptr.hidemyapplist.common.settings_presets + +import android.provider.Settings + +class DeveloperOptionsPreset : BasePreset("dev_options") { + override val settingsKVPairs = listOf( + ReplacementItem( + name = Settings.Global.ADB_ENABLED, + value = "0", + ), + ReplacementItem( + name = "adb_wifi_enabled", + value = "0", + ), + ReplacementItem( + name = Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, + value = "0", + ) + ) +} diff --git a/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/ReplacementItem.kt b/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/ReplacementItem.kt new file mode 100644 index 0000000..5673169 --- /dev/null +++ b/common/src/main/java/icu/nullptr/hidemyapplist/common/settings_presets/ReplacementItem.kt @@ -0,0 +1,11 @@ +package icu.nullptr.hidemyapplist.common.settings_presets + +data class ReplacementItem( + val name: String, + val value: String? +) { + override fun toString() = "ReplacementItem {" + + " 'name': '$name'," + + "'value': '$value'" + + " }" +} \ No newline at end of file diff --git a/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMAService.kt b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMAService.kt index b919747..0837041 100644 --- a/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMAService.kt +++ b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMAService.kt @@ -9,6 +9,7 @@ import icu.nullptr.hidemyapplist.common.Constants import icu.nullptr.hidemyapplist.common.IHMAService import icu.nullptr.hidemyapplist.common.JsonConfig import icu.nullptr.hidemyapplist.common.Utils +import icu.nullptr.hidemyapplist.xposed.hook.AccessibilityHook import icu.nullptr.hidemyapplist.xposed.hook.ActivityHook import icu.nullptr.hidemyapplist.xposed.hook.IFrameworkHook import icu.nullptr.hidemyapplist.xposed.hook.PlatformCompatHook @@ -152,6 +153,7 @@ class HMAService(val pms: IPackageManager) : IHMAService.Stub() { frameworkHooks.add(ActivityHook(this)) frameworkHooks.add(PmsPackageEventsHook(this)) + frameworkHooks.add(AccessibilityHook(this)) frameworkHooks.forEach(IFrameworkHook::load) logI(TAG, "Hooks installed") @@ -159,6 +161,11 @@ class HMAService(val pms: IPackageManager) : IHMAService.Stub() { fun isHookEnabled(packageName: String) = config.scope.containsKey(packageName) + fun getEnabledSettingsPresets(caller: String?): Set { + if (caller == null) return setOf() + return config.scope[caller]?.applySettingsPresets ?: return setOf() + } + fun shouldHide(caller: String?, query: String?): Boolean { if (caller == null || query == null) return false if (caller in Constants.packagesShouldNotHide || query in Constants.packagesShouldNotHide) return false diff --git a/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMASettingsProviderService.kt b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMASettingsProviderService.kt new file mode 100644 index 0000000..ac50e15 --- /dev/null +++ b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/HMASettingsProviderService.kt @@ -0,0 +1,148 @@ +package icu.nullptr.hidemyapplist.xposed + +import android.content.ContentProvider +import android.os.Bundle +import android.provider.Settings +import com.github.kyuubiran.ezxhelper.utils.findMethod +import com.github.kyuubiran.ezxhelper.utils.hookBefore +import de.robv.android.xposed.IXposedHookLoadPackage +import de.robv.android.xposed.callbacks.XC_LoadPackage +import icu.nullptr.hidemyapplist.common.JsonConfig +import icu.nullptr.hidemyapplist.common.SettingsPresets +import icu.nullptr.hidemyapplist.common.Utils +import icu.nullptr.hidemyapplist.common.settings_presets.ReplacementItem +import org.frknkrc44.hma_oss.common.BuildConfig +import java.io.File + +class HMASettingsProviderService : IXposedHookLoadPackage { + companion object { + private const val TAG = "SettingsProviderHook" + } + + init { + searchDataDir() + } + + private lateinit var dataDir: String + private lateinit var configFile: File + + private val configLock = Any() + private var configLastModified: Long = 0 + + var config = JsonConfig().apply { detailLog = true } + private set + + private fun searchDataDir() { + File("/data/system").list()?.forEach { + if (it.startsWith("hide_my_applist")) { + if (!this::dataDir.isInitialized) { + val newDir = File("/data/misc/$it") + File("/data/system/$it").renameTo(newDir) + dataDir = newDir.path + } else { + File("/data/system/$it").deleteRecursively() + } + } + } + File("/data/misc").list()?.forEach { + if (it.startsWith("hide_my_applist")) { + if (!this::dataDir.isInitialized) { + dataDir = "/data/misc/$it" + } else if (dataDir != "/data/misc/$it") { + File("/data/misc/$it").deleteRecursively() + } + } + } + if (!this::dataDir.isInitialized) { + dataDir = "/data/misc/hide_my_applist_" + Utils.generateRandomString(16) + } + + File("$dataDir/log").mkdirs() + configFile = File("$dataDir/config.json") + + logI(TAG, "Data dir: $dataDir") + } + + private fun loadConfig() { + if (!configFile.exists()) { + logI(TAG, "Config file not found") + return + } + + if (configLastModified == configFile.lastModified()) { + return + } + + configLastModified = configFile.lastModified() + + val loading = runCatching { + val json = configFile.readText() + JsonConfig.parse(json) + }.getOrElse { + logE(TAG, "Failed to parse config.json", it) + return + } + if (loading.configVersion != BuildConfig.CONFIG_VERSION) { + logW(TAG, "Config version mismatch, need to reload") + return + } + config = loading + logI(TAG, "Config loaded") + } + + override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { + logI(TAG, "Load hook") + + findMethod( + "com.android.providers.settings.SettingsProvider", + lpparam.classLoader, + ) { + name == "call" + }.hookBefore { param -> + loadConfig() + + val contentProvider = param.thisObject as ContentProvider + val method = param.args[0] as String? + val name = param.args[1] as String? + + val caller = contentProvider.callingPackage + if (!config.scope.containsKey(caller)) { + // logD(TAG, "Not enabled app: $caller") + return@hookBefore + } + + logD(TAG, "@call caller: $caller, method: $method, name: $name") + + when (method) { + "GET_global", "GET_secure", "GET_system" -> { + val replacement = getSpoofedSetting(caller, name) + if (replacement != null) { + logD(TAG, "@getSettings returned replacement for $caller: ${replacement.value}") + param.result = Bundle().apply { + putString(Settings.NameValueTable.VALUE, replacement.value) + } + } + } + } + } + } + + fun getSpoofedSetting(caller: String?, name: String?): ReplacementItem? { + synchronized(configLock) { + if (caller == null || name == null) return null + + val presets = config.scope[caller]?.applySettingsPresets + if (presets?.isEmpty() ?: true) { + return null + } + + for (presetName in presets) { + val preset = SettingsPresets.instance.getPresetByName(presetName) + val value = preset?.getSpoofedValue(name) + if (value != null) return value + } + + return null + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/XposedEntry.kt b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/XposedEntry.kt index 79962ed..802b2b2 100644 --- a/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/XposedEntry.kt +++ b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/XposedEntry.kt @@ -50,6 +50,8 @@ class XposedEntry : IXposedHookZygoteInit, IXposedHookLoadPackage { } } } + } else if (lpparam.packageName == "com.android.providers.settings") { + HMASettingsProviderService().handleLoadPackage(lpparam) } } } diff --git a/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/hook/AccessibilityHook.kt b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/hook/AccessibilityHook.kt new file mode 100644 index 0000000..67b5737 --- /dev/null +++ b/xposed/src/main/java/icu/nullptr/hidemyapplist/xposed/hook/AccessibilityHook.kt @@ -0,0 +1,51 @@ +package icu.nullptr.hidemyapplist.xposed.hook + +import android.accessibilityservice.AccessibilityServiceInfo +import android.app.ActivityThread +import com.github.kyuubiran.ezxhelper.utils.findMethod +import com.github.kyuubiran.ezxhelper.utils.hookBefore +import de.robv.android.xposed.XC_MethodHook +import icu.nullptr.hidemyapplist.common.settings_presets.AccessibilityPreset +import icu.nullptr.hidemyapplist.xposed.HMAService +import icu.nullptr.hidemyapplist.xposed.logE +import icu.nullptr.hidemyapplist.xposed.logI + +class AccessibilityHook(private val service: HMAService) : IFrameworkHook { + companion object { + private const val TAG = "AccessibilityHook" + } + + private val hookList = mutableSetOf() + + override fun load() { + logI(TAG, "Load hook") + + hookList += findMethod( + "com.android.server.accessibility.AccessibilityManagerService" + ) { + name == "getInstalledAccessibilityServiceList" + }.hookBefore { param -> hookedMethod(param) } + + hookList += findMethod( + "com.android.server.accessibility.AccessibilityManagerService" + ) { + name == "getEnabledAccessibilityServiceList" + }.hookBefore { param -> hookedMethod(param) } + } + + private fun hookedMethod(param: XC_MethodHook.MethodHookParam) { + try { + val caller = ActivityThread.currentActivityThread().application.packageName + if (service.getEnabledSettingsPresets(caller).contains(AccessibilityPreset.NAME)) { + param.result = java.util.ArrayList() + } + } catch (e: Throwable) { + logE(TAG, "Fatal error occurred, ignore hooks", e) + } + } + + override fun unload() { + hookList.forEach { it.unhook() } + hookList.clear() + } +} \ No newline at end of file