feat(bixby): add Bixby customization hooks - #39
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for customizing and bypassing restrictions in Samsung Bixby (Agent and Wakeup) using Xposed hooks. It adds options for offline support, custom wakeup phrases, and bypassing wakeup word blacklists, along with the corresponding UI settings. The review feedback highlights critical issues with using beforeHookedMethod instead of afterHookedMethod when hooking SharedPreferencesImpl.getString, which prevents reading the actual stored values. Additionally, there are performance concerns regarding synchronous file I/O in the Hook methods that should be mitigated with caching, and a recommendation to wrap initialization hooks in try-catch blocks to prevent crashes.
| private fun hookInjectModel(lpparam: LoadPackageParam) { | ||
| findAndHookMethod( | ||
| "android.app.SharedPreferencesImpl", | ||
| lpparam.classLoader, | ||
| "getString", | ||
| String::class.java, | ||
| String::class.java, | ||
| object : XC_MethodHook() { | ||
| override fun beforeHookedMethod(param: MethodHookParam) { | ||
| if (param.args[0] == "pref_key_on_device_config_cache") { | ||
| val orig = (param.result ?: param.args[1] ?: "") as String | ||
| if (!orig.contains(Build.MODEL)) { | ||
| param.result = if (orig.isEmpty()) Build.MODEL else "$orig,${Build.MODEL}" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ) | ||
| } |
There was a problem hiding this comment.
在 beforeHookedMethod 中,param.result 初始为 null。因此,val orig = (param.result ?: param.args[1] ?: "") as String 将始终回退到默认值(param.args[1]),从而完全忽略了 SharedPreferences 中实际存储的值。此外,在 beforeHookedMethod 中设置 param.result 会导致原始的 getString 方法被跳过,使得真实的配置无法被读取。建议将此 Hook 改为 afterHookedMethod,此时 param.result 已包含从 SharedPreferences 中读取到的真实值,然后再对其进行修改和重新赋值。
| private fun hookInjectModel(lpparam: LoadPackageParam) { | |
| findAndHookMethod( | |
| "android.app.SharedPreferencesImpl", | |
| lpparam.classLoader, | |
| "getString", | |
| String::class.java, | |
| String::class.java, | |
| object : XC_MethodHook() { | |
| override fun beforeHookedMethod(param: MethodHookParam) { | |
| if (param.args[0] == "pref_key_on_device_config_cache") { | |
| val orig = (param.result ?: param.args[1] ?: "") as String | |
| if (!orig.contains(Build.MODEL)) { | |
| param.result = if (orig.isEmpty()) Build.MODEL else "$orig,${Build.MODEL}" | |
| } | |
| } | |
| } | |
| } | |
| ) | |
| } | |
| private fun hookInjectModel(lpparam: LoadPackageParam) { | |
| findAndHookMethod( | |
| "android.app.SharedPreferencesImpl", | |
| lpparam.classLoader, | |
| "getString", | |
| String::class.java, | |
| String::class.java, | |
| object : XC_MethodHook() { | |
| override fun afterHookedMethod(param: MethodHookParam) { | |
| if (param.args[0] == "pref_key_on_device_config_cache") { | |
| val orig = (param.result ?: param.args[1] ?: "") as String | |
| if (!orig.contains(Build.MODEL)) { | |
| param.result = if (orig.isEmpty()) Build.MODEL else "$orig,${Build.MODEL}" | |
| } | |
| } | |
| } | |
| } | |
| ) | |
| } |
| private fun hookWakeupCustomPhrase(lpparam: LoadPackageParam) { | ||
| findAndHookMethod( | ||
| "android.app.SharedPreferencesImpl", | ||
| lpparam.classLoader, | ||
| "getString", | ||
| String::class.java, | ||
| String::class.java, | ||
| object : XC_MethodHook() { | ||
| override fun beforeHookedMethod(param: MethodHookParam) { | ||
| if (param.args[0] == "myvoice_string_custom") { | ||
| val orig = param.result ?: param.args[1] ?: "" | ||
| if (orig.toString().isEmpty()) { | ||
| val txt = readWakeupSP("myvoice_string_custom") | ||
| if (txt.isNotEmpty()) param.result = txt | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ) | ||
| try { | ||
| val cursorClass = lpparam.classLoader.loadClass("android.database.MatrixCursor") | ||
| findAndHookMethod( | ||
| cursorClass, | ||
| "addRow", | ||
| arrayOfNulls<Any>(0).javaClass, | ||
| object : XC_MethodHook() { | ||
| override fun beforeHookedMethod(param: MethodHookParam) { | ||
| val row = param.args[0] as? Array<Any?> ?: return | ||
| try { | ||
| val cols = param.thisObject.javaClass | ||
| .getDeclaredField("columnNames") | ||
| .apply { isAccessible = true } | ||
| .get(param.thisObject) as? Array<String> ?: return | ||
| for (i in cols.indices) { | ||
| if (cols[i] != "customKeyword") continue | ||
| if (row[i] == null || row[i].toString().isEmpty()) { | ||
| val txt = readWakeupSP("myvoice_string_custom") | ||
| if (txt.isNotEmpty()) row[i] = txt | ||
| } | ||
| } | ||
| } catch (_: Throwable) { | ||
| } | ||
| } | ||
| } | ||
| ) | ||
| } catch (_: Throwable) { | ||
| } | ||
| } |
There was a problem hiding this comment.
同样地,这里在 beforeHookedMethod 中拦截 getString 会导致无法读取到 SharedPreferences 中实际存储的自定义唤醒词。如果 param.result 为 null 且默认值为空,设置 param.result 将直接跳过原始方法的执行。建议将此 Hook 改为 afterHookedMethod,以便在原始方法执行完毕并获取到真实值后再进行判断和覆盖。
private fun hookWakeupCustomPhrase(lpparam: LoadPackageParam) {
findAndHookMethod(
"android.app.SharedPreferencesImpl",
lpparam.classLoader,
"getString",
String::class.java,
String::class.java,
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
if (param.args[0] == "myvoice_string_custom") {
val orig = param.result ?: param.args[1] ?: ""
if (orig.toString().isEmpty()) {
val txt = readWakeupSP("myvoice_string_custom")
if (txt.isNotEmpty()) param.result = txt
}
}
}
}
)
try {
val cursorClass = lpparam.classLoader.loadClass("android.database.MatrixCursor")
findAndHookMethod(
cursorClass,
"addRow",
arrayOfNulls<Any>(0).javaClass,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
val row = param.args[0] as? Array<Any?> ?: return
try {
val cols = param.thisObject.javaClass
.getDeclaredField("columnNames")
.apply { isAccessible = true }
.get(param.thisObject) as? Array<String> ?: return
for (i in cols.indices) {
if (cols[i] != "customKeyword") continue
if (row[i] == null || row[i].toString().isEmpty()) {
val txt = readWakeupSP("myvoice_string_custom")
if (txt.isNotEmpty()) row[i] = txt
}
}
} catch (_: Throwable) {
}
}
}
)
} catch (_: Throwable) {
}
}| private fun readWakeupSP(key: String): String { | ||
| try { | ||
| val dir = File("/data/data/com.samsung.android.bixby.wakeup/shared_prefs") | ||
| if (!dir.exists() || !dir.isDirectory) return "" | ||
| for (file in dir.listFiles() ?: emptyArray()) { | ||
| if (!file.name.endsWith(".xml")) continue | ||
| val match = Regex("<string name=\"$key\">(.*?)</string>").find(file.readText()) | ||
| if (match != null) return match.groupValues[1] | ||
| } | ||
| } catch (_: Throwable) { | ||
| } | ||
| return "" | ||
| } | ||
| } |
There was a problem hiding this comment.
在 getString 和 addRow 的 Hook 中频繁调用 readWakeupSP 会导致大量的同步文件 I/O 操作(file.readText())和正则表达式匹配。由于这些 Hook 通常在主线程或 Binder 线程中执行,这会带来严重的性能问题,甚至导致 UI 卡顿或 ANR。建议对读取到的自定义唤醒词进行内存缓存(例如使用一个私有的 cachedCustomPhrase 变量),避免每次调用都重新读取和解析文件。
private var cachedCustomPhrase: String? = null
private fun readWakeupSP(key: String): String {
if (key == "myvoice_string_custom" && cachedCustomPhrase != null) {
return cachedCustomPhrase!!
}
try {
val dir = File("/data/data/com.samsung.android.bixby.wakeup/shared_prefs")
if (!dir.exists() || !dir.isDirectory) return ""
for (file in dir.listFiles() ?: emptyArray()) {
if (!file.name.endsWith(".xml")) continue
val match = Regex("""<string name="$key">(.*?)</string>""").find(file.readText())
if (match != null) {
val value = match.groupValues[1]
if (key == "myvoice_string_custom") {
cachedCustomPhrase = value
}
return value
}
}
} catch (_: Throwable) {
}
return ""
}
}| private fun initBixbyWakeup(lpparam: LoadPackageParam, wwvBypass: Boolean) { | ||
| hookWakeupCustomPhrase(lpparam) | ||
| if (wwvBypass) { | ||
| hookWakeupWordTypeValidator(lpparam) | ||
| hookKwdCjkFix(lpparam) | ||
| } | ||
| } |
There was a problem hiding this comment.
与 initBixbyAgent 类似,建议将 initBixbyWakeup 中的各个 Hook 调用也包裹在 try-catch 块中,并记录日志。这样可以防止其中某一个 Hook 失败(例如类不存在或反射异常)时导致整个初始化流程中断或目标应用崩溃,提高代码的健壮性。
private fun initBixbyWakeup(lpparam: LoadPackageParam, wwvBypass: Boolean) {
try {
hookWakeupCustomPhrase(lpparam)
} catch (e: Throwable) {
XposedBridge.log("[OneUIX-Bixby] [-] wakeupCustomPhrase: ${e.message}")
}
if (wwvBypass) {
try {
hookWakeupWordTypeValidator(lpparam)
} catch (e: Throwable) {
XposedBridge.log("[OneUIX-Bixby] [-] wakeupWordTypeValidator: ${e.message}")
}
try {
hookKwdCjkFix(lpparam)
} catch (e: Throwable) {
XposedBridge.log("[OneUIX-Bixby] [-] kwdCjkFix: ${e.message}")
}
}
}- Add Bixby offline support: inject device model to bypass restrictions - Add custom wakeup phrase: bypass Labs feature check - Add wakeup word blacklist bypass: skip native word validation - Add Bixby settings UI with toggle switches - Register Bixby hooks in Main.kt for both Bixby Agent and Wakeup packages
- Update Bixby.kt to match Self branch (use Preference.Bixby parameter) - Add Chinese, Russian, French translations for Bixby strings
|
这个功能很好啊,期待以后有PR |
功能说明
添加 Bixby 相关的自定义 Hook 功能:
新增功能
修改文件