Skip to content

feat(bixby): add Bixby customization hooks - #39

Closed
Mzdyl wants to merge 2 commits into
SoClear:mainfrom
Mzdyl:feat/bixby
Closed

feat(bixby): add Bixby customization hooks#39
Mzdyl wants to merge 2 commits into
SoClear:mainfrom
Mzdyl:feat/bixby

Conversation

@Mzdyl

@Mzdyl Mzdyl commented May 26, 2026

Copy link
Copy Markdown
Contributor

功能说明

添加 Bixby 相关的自定义 Hook 功能:

新增功能

  • 离线支持: 注入设备型号到白名单缓存,绕过 Bixby 离线限制
  • 自定义唤醒词: 绕过 Labs 功能检查,启用自定义唤醒短语
  • 唤醒词黑名单绕过: 跳过原生词库黑名单验证,支持竞品词/敏感词

修改文件

  • : 添加 BIXBY_AGENT 和 BIXBY_WAKEUP 包名常量
  • : 添加 Bixby 数据类
  • : 新增 Bixby Hook 实现
  • : 注册 Bixby Hook
  • : 添加 Bixby 分类
  • : 新增 Bixby 设置界面
  • : 集成 Bixby 设置
  • : 添加字符串资源

@Mzdyl
Mzdyl marked this pull request as draft May 26, 2026 18:06

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +67 to +85
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}"
}
}
}
}
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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 中读取到的真实值,然后再对其进行修改和重新赋值。

Suggested change
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}"
}
}
}
}
)
}

Comment on lines +225 to +272
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) {
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

同样地,这里在 beforeHookedMethod 中拦截 getString 会导致无法读取到 SharedPreferences 中实际存储的自定义唤醒词。如果 param.resultnull 且默认值为空,设置 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) {
        }
    }

Comment on lines +274 to +287
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 ""
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

getStringaddRow 的 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 ""
    }
}

Comment on lines +59 to +65
private fun initBixbyWakeup(lpparam: LoadPackageParam, wwvBypass: Boolean) {
hookWakeupCustomPhrase(lpparam)
if (wwvBypass) {
hookWakeupWordTypeValidator(lpparam)
hookKwdCjkFix(lpparam)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
@Mzdyl Mzdyl closed this May 26, 2026
@Mzdyl
Mzdyl deleted the feat/bixby branch May 26, 2026 18:22
@SoClear

SoClear commented May 27, 2026

Copy link
Copy Markdown
Owner

这个功能很好啊,期待以后有PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants