Skip to content

Commit 59f7717

Browse files
committed
implement HideAbi module
1 parent 57c76a9 commit 59f7717

File tree

19 files changed

+481
-0
lines changed

19 files changed

+481
-0
lines changed

HideAbi/Readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# HideAbi
2+
3+
Hide specific CPU ABIs from apps.
4+
5+
This module hooks `android.os.Build.SUPPORTED_{,{64,32}_BIT_}ABIS` to filter out selected ABIs from being reported to applications.
6+
Useful for controlling which architecture apps use or convincing an app store to download the correct apk.

HideAbi/build.gradle.kts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
plugins {
2+
alias(libs.plugins.buildlogic.android.application)
3+
alias(libs.plugins.buildlogic.kotlin.android)
4+
}
5+
6+
android {
7+
namespace = "de.binarynoise.HideAbi"
8+
9+
defaultConfig {
10+
minSdk = 21
11+
targetSdk = 36
12+
buildConfigField("String", "SHARED_PREFERENCES_NAME", "\"hide_abi\"")
13+
}
14+
15+
buildFeatures {
16+
buildConfig = true
17+
}
18+
}
19+
20+
dependencies {
21+
implementation(projects.reflection)
22+
implementation(projects.logger)
23+
implementation(libs.hiddenapibypass)
24+
implementation(libs.androidx.fragment.ktx)
25+
implementation(libs.androidx.preference.ktx)
26+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest
3+
xmlns:android="http://schemas.android.com/apk/res/android">
4+
5+
<application android:theme="@android:style/Theme.DeviceDefault.Settings">
6+
<activity
7+
android:name=".AbisActivity"
8+
android:exported="true"
9+
>
10+
<intent-filter>
11+
<action android:name="android.intent.action.MAIN" />
12+
<action android:name="android.intent.action.VIEW" />
13+
14+
<category android:name="android.intent.category.LAUNCHER" />
15+
<category android:name="android.intent.category.DEFAULT" />
16+
</intent-filter>
17+
</activity>
18+
</application>
19+
</manifest>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package de.binarynoise.HideAbi
2+
3+
import android.os.Build
4+
import android.os.Bundle
5+
import android.widget.TextView
6+
import androidx.activity.ComponentActivity
7+
import de.binarynoise.HideAbi.BuildConfig.SHARED_PREFERENCES_NAME
8+
9+
class AbisActivity : ComponentActivity(R.layout.abis_activity) {
10+
override fun onCreate(savedInstanceState: Bundle?) {
11+
super.onCreate(savedInstanceState)
12+
findViewById<TextView>(R.id.supported_abis).text = Build.SUPPORTED_ABIS.joinToString("\n")
13+
val sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_WORLD_READABLE)
14+
findViewById<TextView>(R.id.preferences).text = sharedPreferences.all.entries.joinToString("\n") { (k, v) -> "$k -> $v" }
15+
}
16+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout
3+
xmlns:android="http://schemas.android.com/apk/res/android"
4+
xmlns:tools="http://schemas.android.com/tools"
5+
android:layout_width="match_parent"
6+
android:layout_height="match_parent"
7+
android:orientation="vertical"
8+
android:padding="8dp"
9+
>
10+
11+
<TextView
12+
android:layout_width="match_parent"
13+
android:layout_height="wrap_content"
14+
android:text="SUPPORTED_ABIS:"
15+
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium"
16+
/>
17+
18+
<TextView
19+
android:id="@+id/supported_abis"
20+
android:layout_width="match_parent"
21+
android:layout_height="wrap_content"
22+
android:textAppearance="@android:style/TextAppearance.DeviceDefault"
23+
tools:text="arm64\narm"
24+
/>
25+
26+
27+
<TextView
28+
android:layout_width="match_parent"
29+
android:layout_height="wrap_content"
30+
android:text="Preferences:"
31+
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium"
32+
/>
33+
34+
35+
<TextView
36+
android:id="@+id/preferences"
37+
android:layout_width="match_parent"
38+
android:layout_height="wrap_content"
39+
android:textAppearance="@android:style/TextAppearance.DeviceDefault"
40+
tools:text="whatever"
41+
/>
42+
43+
</LinearLayout>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest
3+
xmlns:android="http://schemas.android.com/apk/res/android">
4+
5+
<application
6+
android:icon="@mipmap/ic_launcher"
7+
android:label="HideAbi"
8+
android:theme="@android:style/Theme.DeviceDefault.Settings"
9+
>
10+
<meta-data
11+
android:name="xposedmodule"
12+
android:value="true"
13+
/>
14+
<meta-data
15+
android:name="xposeddescription"
16+
android:value="Hide ABIs from apps"
17+
/>
18+
<meta-data
19+
android:name="xposedminversion"
20+
android:value="93"
21+
/>
22+
<meta-data
23+
android:name="xposedscope"
24+
android:resource="@array/scope"
25+
/>
26+
27+
<activity
28+
android:name=".SettingsActivity"
29+
android:exported="true"
30+
>
31+
<intent-filter>
32+
<action android:name="android.intent.action.MAIN" />
33+
<category android:name="de.robv.android.xposed.category.MODULE_SETTINGS" />
34+
</intent-filter>
35+
</activity>
36+
</application>
37+
38+
</manifest>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
de.binarynoise.HideAbi.Hook
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package de.binarynoise.HideAbi
2+
3+
import android.os.Build
4+
5+
enum class AbiCategory(val property: String, val getter: () -> Array<String>) {
6+
SUPPORTED_ABIS("ro.product.cpu.abilist", { Build.SUPPORTED_ABIS }),
7+
SUPPORTED_64_BIT_ABIS("ro.product.cpu.abilist64", { Build.SUPPORTED_64_BIT_ABIS }),
8+
SUPPORTED_32_BIT_ABIS("ro.product.cpu.abilist32", { Build.SUPPORTED_32_BIT_ABIS }),
9+
;
10+
11+
fun getPreferenceKeyFor(abi: String): String {
12+
return "${this.property}_$abi"
13+
}
14+
15+
companion object {
16+
fun fromProperty(property: String): AbiCategory? {
17+
return entries.find { it.property == property }
18+
}
19+
}
20+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package de.binarynoise.HideAbi
2+
3+
import java.lang.reflect.Method
4+
import android.os.Build
5+
import de.binarynoise.reflection.cast
6+
import de.binarynoise.reflection.makeAccessible
7+
import de.robv.android.xposed.IXposedHookLoadPackage
8+
import de.robv.android.xposed.XSharedPreferences
9+
import de.robv.android.xposed.XposedBridge
10+
import de.robv.android.xposed.XposedHelpers
11+
import de.robv.android.xposed.callbacks.XC_LoadPackage
12+
import org.lsposed.hiddenapibypass.HiddenApiBypass
13+
import de.robv.android.xposed.XC_MethodHook as MethodHook
14+
15+
class Hook : IXposedHookLoadPackage {
16+
17+
18+
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
19+
val BuildClass = XposedHelpers.findClass("android.os.Build", lpparam.classLoader)
20+
val getStringList: Method = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
21+
HiddenApiBypass.getDeclaredMethod(BuildClass, "getStringList", String::class.java, String::class.java)
22+
} else {
23+
BuildClass.getDeclaredMethod("getStringList", String::class.java, String::class.java)
24+
}
25+
getStringList.makeAccessible()
26+
27+
val prefs = XSharedPreferences(BuildConfig.APPLICATION_ID, BuildConfig.SHARED_PREFERENCES_NAME)
28+
29+
XposedBridge.hookMethod(
30+
getStringList,
31+
object : MethodHook() {
32+
override fun afterHookedMethod(param: MethodHookParam) {
33+
with(param) {
34+
val property = args.first().cast<String>()
35+
val abiCategory = AbiCategory.fromProperty(property) ?: return
36+
37+
val result = param.result.cast<Array<String>>()
38+
val filtered = result.filterNot { abi ->
39+
prefs.getBoolean(abiCategory.getPreferenceKeyFor(abi), false)
40+
}
41+
param.result = filtered.toTypedArray()
42+
}
43+
}
44+
},
45+
)
46+
47+
AbiCategory.entries.forEach { entry ->
48+
XposedHelpers.setStaticObjectField(Build::class.java, entry.name, getStringList(null, entry.property, ","))
49+
}
50+
}
51+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package de.binarynoise.HideAbi
2+
3+
import kotlin.contracts.ExperimentalContracts
4+
import kotlin.contracts.InvocationKind
5+
import kotlin.contracts.contract
6+
import android.content.Context
7+
import android.os.Bundle
8+
import androidx.fragment.app.FragmentActivity
9+
import androidx.preference.CheckBoxPreference
10+
import androidx.preference.Preference
11+
import androidx.preference.PreferenceCategory
12+
import androidx.preference.PreferenceFragmentCompat
13+
import androidx.preference.PreferenceGroup
14+
import androidx.preference.children
15+
import de.binarynoise.HideAbi.BuildConfig.SHARED_PREFERENCES_NAME
16+
import de.binarynoise.reflection.cast
17+
18+
19+
class SettingsActivity : FragmentActivity() {
20+
override fun onCreate(savedInstanceState: Bundle?) {
21+
super.onCreate(savedInstanceState)
22+
setContentView(R.layout.settings_activity)
23+
if (savedInstanceState == null) {
24+
supportFragmentManager.beginTransaction().replace(R.id.settings, SettingsFragment()).commit()
25+
}
26+
actionBar?.setDisplayHomeAsUpEnabled(true)
27+
}
28+
29+
override fun onNavigateUp(): Boolean {
30+
finishAndRemoveTask()
31+
return true
32+
}
33+
34+
class SettingsFragment : PreferenceFragmentCompat() {
35+
36+
37+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
38+
preferenceManager.sharedPreferencesName = SHARED_PREFERENCES_NAME
39+
preferenceManager.sharedPreferencesMode = MODE_WORLD_READABLE
40+
41+
val ctx = preferenceManager.context
42+
preferenceScreen = preferenceManager.createPreferenceScreen(ctx)
43+
preferenceScreen.apply {
44+
for (category in AbiCategory.entries) {
45+
addPreference(PreferenceCategory(ctx)) {
46+
title = category.name
47+
val abis = ctx.getHardwareABIs(category)
48+
for (abi in abis) {
49+
addPreference(CheckBoxPreference(ctx)) {
50+
key = category.getPreferenceKeyFor(abi)
51+
title = abi
52+
summaryOn = "This ABI will be hidden and not reported to apps"
53+
summaryOff = "This ABI will be reported to apps"
54+
}
55+
}
56+
}
57+
}
58+
setIconSpaceReservedRecursive(false)
59+
}
60+
}
61+
62+
/**
63+
* Returns the hardware ABIs for the given property.
64+
* Combines the ABIs we currently see with the ones that are hooked from the preferences (and can't see).
65+
*/
66+
fun Context.getHardwareABIs(property: AbiCategory): Set<String> {
67+
val sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_WORLD_READABLE)
68+
69+
return (property.getter() + sharedPreferences.all //
70+
.filter { (k, v) -> k.startsWith(property.property) && (v?.cast<Boolean?>() == true) } //
71+
.keys //
72+
.map { it.substringAfter("_") } //
73+
.toTypedArray()) //
74+
.toSet()
75+
}
76+
77+
78+
@OptIn(ExperimentalContracts::class)
79+
inline fun <T : Preference> PreferenceGroup.addPreference(preference: T, setup: T.() -> Unit) {
80+
contract {
81+
callsInPlace(setup, InvocationKind.EXACTLY_ONCE)
82+
}
83+
84+
val isPreferenceGroup = preference is PreferenceGroup
85+
86+
if (isPreferenceGroup) {
87+
// PreferenceGroup needs to be added to the tree before other preferences can be added to it
88+
addPreference(preference)
89+
}
90+
91+
preference.apply(setup)
92+
93+
if (!isPreferenceGroup) {
94+
// normal preferences need the setup applied before being added to the tree
95+
addPreference(preference)
96+
}
97+
}
98+
99+
private fun Preference.setIconSpaceReservedRecursive(iconSpaceReserved: Boolean = false) {
100+
this.isIconSpaceReserved = iconSpaceReserved
101+
if (this is PreferenceGroup) this.children.forEach { it.setIconSpaceReservedRecursive(iconSpaceReserved) }
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)