Add "Settings Presets"

This commit is contained in:
frknkrc44 2025-08-27 01:24:46 +03:00
commit c763b054f3
No known key found for this signature in database
GPG key ID: 784AE1CC0DCFD34F
15 changed files with 351 additions and 4 deletions

View file

@ -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)
}
}
}

View file

@ -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<Preference>("applySettingsPresets")?.title =
getString(R.string.app_settings_preset_using, pack.config.applySettingsPresets.size)
}
private fun updateExtraAppList(useWhiteList: Boolean) {
findPreference<Preference>("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<Preference>("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<Preference>("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)
}
}

View file

@ -88,6 +88,7 @@
<string name="app_exclude_system_apps_summary">Turn off this option only when really necessary. It is likely to cause crash with it disabled</string>
<string name="app_template_using">Using %d templates</string>
<string name="app_preset_using">Using %d presets</string>
<string name="app_settings_preset_using">Using %d settings presets</string>
<string name="app_extra_apps_visible_count">%d additional apps visible</string>
<string name="app_extra_apps_invisible_count">%d additional apps invisible</string>
@ -96,6 +97,10 @@
<string name="preset_root_apps">Root managers/Rooted apps</string>
<string name="preset_custom_rom">Custom ROM apps</string>
<!-- Settings preset names -->
<string name="settings_preset_accessibility">Accessibility</string>
<string name="settings_preset_dev_options">Developer options</string>
<!-- Logs -->
<string name="logs_save">Save logs</string>
<string name="logs_clear">Clear logs</string>

View file

@ -38,6 +38,10 @@
android:icon="@drawable/outline_assignment_24"
android:key="applyPresets"
tools:title="@string/app_preset_using" />
<Preference
android:icon="@drawable/outline_settings_24"
android:key="applySettingsPresets"
tools:title="@string/app_settings_preset_using" />
<Preference
android:icon="@drawable/baseline_apps_24"
android:key="extraAppList"

View file

@ -36,7 +36,7 @@ val targetSdkVer by extra(36)
val appVerCode = gitCommitCount + 0x6f7373
val appVerName by extra("3.3.1-oss-${gitCommitCountAfterOss}${gitHasUncommittedSuffix}")
val configVerCode by extra(91)
val configVerCode by extra(92)
val serviceVerCode by extra(98)
val minBackupVerCode by extra(65)

View file

@ -33,6 +33,7 @@ data class JsonConfig(
var excludeSystemApps: Boolean = true,
var applyTemplates: MutableSet<String> = mutableSetOf(),
var applyPresets: MutableSet<String> = mutableSetOf(),
var applySettingsPresets: MutableSet<String> = mutableSetOf(),
var extraAppList: MutableSet<String> = mutableSetOf()
) {
override fun toString() = encoder.encodeToString(this)

View file

@ -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<BasePreset>()
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<String>) = presetList.filter { names.contains(it.name) }
fun getPresetByName(name: String) = presetList.firstOrNull { it.name == name }
init {
presetList.add(DeveloperOptionsPreset())
presetList.add(AccessibilityPreset())
}
}

View file

@ -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,
),
)
}

View file

@ -0,0 +1,7 @@
package icu.nullptr.hidemyapplist.common.settings_presets
abstract class BasePreset(val name: String) {
protected abstract val settingsKVPairs: List<ReplacementItem>
fun getSpoofedValue(key: String) = settingsKVPairs.firstOrNull { it.name == key }
}

View file

@ -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",
)
)
}

View file

@ -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'" +
" }"
}

View file

@ -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<String> {
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

View file

@ -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
}
}
}

View file

@ -50,6 +50,8 @@ class XposedEntry : IXposedHookZygoteInit, IXposedHookLoadPackage {
}
}
}
} else if (lpparam.packageName == "com.android.providers.settings") {
HMASettingsProviderService().handleLoadPackage(lpparam)
}
}
}

View file

@ -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<XC_MethodHook.Unhook>()
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<AccessibilityServiceInfo>()
}
} catch (e: Throwable) {
logE(TAG, "Fatal error occurred, ignore hooks", e)
}
}
override fun unload() {
hookList.forEach { it.unhook() }
hookList.clear()
}
}