From 3e9305a5aec91922e75e163123a556949a6b87b2 Mon Sep 17 00:00:00 2001 From: Mzdyl <25149539+Mzdyl@users.noreply.github.com> Date: Wed, 27 May 2026 02:05:15 +0800 Subject: [PATCH 1/2] feat(bixby): add Bixby customization hooks - 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 --- .../io/github/soclear/oneuix/data/Package.kt | 2 + .../github/soclear/oneuix/data/Preference.kt | 8 + .../io/github/soclear/oneuix/hook/Bixby.kt | 287 ++++++++++++++++++ .../io/github/soclear/oneuix/hook/Main.kt | 11 + .../github/soclear/oneuix/ui/SettingScreen.kt | 7 + .../soclear/oneuix/ui/category/Category.kt | 1 + .../oneuix/ui/category/DetailPaneBixby.kt | 66 ++++ app/src/main/res/values/strings.xml | 6 + 8 files changed, 388 insertions(+) create mode 100644 app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt create mode 100644 app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneBixby.kt diff --git a/app/src/main/java/io/github/soclear/oneuix/data/Package.kt b/app/src/main/java/io/github/soclear/oneuix/data/Package.kt index 277607e..c483373 100644 --- a/app/src/main/java/io/github/soclear/oneuix/data/Package.kt +++ b/app/src/main/java/io/github/soclear/oneuix/data/Package.kt @@ -2,6 +2,8 @@ package io.github.soclear.oneuix.data object Package { const val ANDROID = "android" + const val BIXBY_AGENT = "com.samsung.android.bixby.agent" + const val BIXBY_WAKEUP = "com.samsung.android.bixby.wakeup" const val BROWSER = "com.sec.android.app.sbrowser" const val CALENDAR = "com.samsung.android.calendar" const val CAMERA = "com.sec.android.app.camera" diff --git a/app/src/main/java/io/github/soclear/oneuix/data/Preference.kt b/app/src/main/java/io/github/soclear/oneuix/data/Preference.kt index b42105b..3342167 100644 --- a/app/src/main/java/io/github/soclear/oneuix/data/Preference.kt +++ b/app/src/main/java/io/github/soclear/oneuix/data/Preference.kt @@ -10,6 +10,7 @@ data class Preference( val settings: Settings = Settings(), val call: Call = Call(), val camera: Camera = Camera(), + val bixby: Bixby = Bixby(), val other: Other = Other(), ) { @Serializable @@ -113,6 +114,13 @@ data class Preference( val disableCameraTemperatureCheck: Boolean = false, ) + @Serializable + data class Bixby( + val injectModel: Boolean = false, + val labsMgr: Boolean = false, + val wwvBypass: Boolean = false, + ) + @Serializable data class Other( val blockGalaxyStoreAds: Boolean = true, diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt b/app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt new file mode 100644 index 0000000..34a8a1c --- /dev/null +++ b/app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt @@ -0,0 +1,287 @@ +package io.github.soclear.oneuix.hook + +import android.content.Context +import android.os.Build +import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedHelpers.findAndHookMethod +import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam +import io.github.soclear.oneuix.data.Package +import java.io.File +import java.lang.reflect.Modifier +import java.util.Locale + +object Bixby { + + fun init(lpparam: LoadPackageParam, injectModel: Boolean, labsMgr: Boolean, wwvBypass: Boolean) { + when (lpparam.packageName) { + Package.BIXBY_AGENT -> initBixbyAgent(lpparam, injectModel, labsMgr, wwvBypass) + Package.BIXBY_WAKEUP -> initBixbyWakeup(lpparam, wwvBypass) + } + } + + private fun initBixbyAgent( + lpparam: LoadPackageParam, + injectModel: Boolean, + labsMgr: Boolean, + wwvBypass: Boolean, + ) { + XposedBridge.log("[OneUIX-Bixby] Init injectModel=$injectModel labsMgr=$labsMgr wwvBypass=$wwvBypass") + + if (injectModel) { + try { + hookInjectModel(lpparam) + XposedBridge.log("[OneUIX-Bixby] [+] injectModel") + } catch (e: Throwable) { + XposedBridge.log("[OneUIX-Bixby] [-] injectModel: ${e.message}") + } + } + + if (labsMgr) { + try { + hookLabsFeatureManager(lpparam) + XposedBridge.log("[OneUIX-Bixby] [+] labsFeatureManager") + } catch (e: Throwable) { + XposedBridge.log("[OneUIX-Bixby] [-] labsFeatureManager: ${e.message}") + } + } + + if (wwvBypass) { + try { + hookWakeupWordValidator(lpparam) + XposedBridge.log("[OneUIX-Bixby] [+] wakeupWordValidator") + } catch (e: Throwable) { + XposedBridge.log("[OneUIX-Bixby] [-] wakeupWordValidator: ${e.message}") + } + } + } + + private fun initBixbyWakeup(lpparam: LoadPackageParam, wwvBypass: Boolean) { + hookWakeupCustomPhrase(lpparam) + if (wwvBypass) { + hookWakeupWordTypeValidator(lpparam) + hookKwdCjkFix(lpparam) + } + } + + 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 hookLabsFeatureManager(lpparam: LoadPackageParam) { + try { + val clazz = Class.forName( + "com.samsung.android.bixby.agent.common.util.datamanager.LabsFeatureManager", + true, + lpparam.classLoader, + ) + for (name in arrayOf("isSupported", "isAvailable", "isEnabled", "isLabs")) { + findAndHookMethod( + clazz, + name, + String::class.java, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + if (param.args[0] == "labs_custom_wakeup") { + param.result = true + } + } + } + ) + } + findAndHookMethod( + clazz, + "isLabsMenuSupported", + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + param.result = true + } + } + ) + } catch (_: Throwable) { + } + } + + private fun hookWakeupWordValidator(lpparam: LoadPackageParam) { + val clazz = lpparam.classLoader.loadClass("com.samsung.voicewakeup.wwv.WakeupWordValidator") + for (method in clazz.declaredMethods) { + if (!Modifier.isPublic(method.modifiers)) continue + val pts = method.parameterTypes + if (method.returnType == Boolean::class.javaPrimitiveType && + pts.contentEquals(arrayOf(Locale::class.java, String::class.java, String::class.java, String::class.java)) + ) { + XposedBridge.hookMethod( + method, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + param.result = true + } + } + ) + } + if (method.returnType == Int::class.javaPrimitiveType && + pts.contentEquals(arrayOf(Context::class.java, String::class.java, Locale::class.java, String::class.java)) + ) { + XposedBridge.hookMethod( + method, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + param.result = 0 + } + } + ) + } + } + } + + private fun hookWakeupWordTypeValidator(lpparam: LoadPackageParam) { + for (className in arrayOf( + "com.samsung.voicewakeup.kwv.normal.custom.WakeupKwvNormalCommon", + "com.samsung.voicewakeup.kwv.bargein.custom.WakeupKwvBargeinCommon", + "com.samsung.voicewakeup.kwv.acousticecho.custom.WakeupKwvAcousticEchoCommon", + )) { + try { + val clazz = lpparam.classLoader.loadClass(className) + for (method in clazz.declaredMethods) { + if (method.returnType != Boolean::class.javaPrimitiveType) continue + if (!method.parameterTypes.contentEquals(arrayOf(String::class.java, Locale::class.java))) continue + XposedBridge.hookMethod( + method, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + param.result = true + } + } + ) + } + } catch (_: Throwable) { + } + } + } + + private fun hookKwdCjkFix(lpparam: LoadPackageParam) { + for (className in arrayOf( + "com.samsung.voicewakeup.kwd.normal.custom.WakeupKwdNormalCustom", + "com.samsung.voicewakeup.kwd.bargein.custom.WakeupKwdBargeinCustom", + "com.samsung.voicewakeup.kwd.acousticecho.custom.WakeupKwdAcousticEchoCustom", + )) { + try { + val clazz = lpparam.classLoader.loadClass(className) + var kwField: java.lang.reflect.Field? = null + try { + kwField = clazz.getDeclaredField("mKeyword") + kwField.isAccessible = true + } catch (_: Throwable) { + } + + for (method in clazz.declaredMethods) { + if (method.returnType != Int::class.javaPrimitiveType) continue + val pts = method.parameterTypes + val isVr = (pts.size == 1 && pts[0].isArray && pts[0].componentType == Short::class.javaPrimitiveType) || + (pts.size == 3 && pts[0].isArray && pts[0].componentType == Short::class.javaPrimitiveType && + pts[1] == Int::class.javaPrimitiveType && pts[2] == Int::class.javaPrimitiveType) + if (!isVr) continue + + val kwRef = kwField + XposedBridge.hookMethod( + method, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + val ret = param.result as? Int ?: return + if (ret != 0) return + val kw = try { + kwRef?.get(param.thisObject) ?: "" + } catch (_: Throwable) { + "" + } + if ((kw as? String)?.any { it in '\u4E00'..'\u9FFF' } == true) { + param.result = 1 + } + } + } + ) + } + } catch (_: Throwable) { + } + } + } + + 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(0).javaClass, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + val row = param.args[0] as? Array ?: return + try { + val cols = param.thisObject.javaClass + .getDeclaredField("columnNames") + .apply { isAccessible = true } + .get(param.thisObject) as? Array ?: 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("(.*?)").find(file.readText()) + if (match != null) return match.groupValues[1] + } + } catch (_: Throwable) { + } + return "" + } +} diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt b/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt index fe45f18..06da347 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt @@ -389,6 +389,17 @@ class Main : IXposedHookLoadPackage, IXposedHookInitPackageResources, IXposedHoo } } + Package.BIXBY_AGENT, Package.BIXBY_WAKEUP -> { + if (preference.bixby.injectModel || preference.bixby.labsMgr || preference.bixby.wwvBypass) { + Bixby.init( + lpparam = lpparam, + injectModel = preference.bixby.injectModel, + labsMgr = preference.bixby.labsMgr, + wwvBypass = preference.bixby.wwvBypass, + ) + } + } + "com.samsung.android.service.airviewdictionary" -> { if (preference.other.useSPenGoogleTranslate) { SPen.switchTranslateSource(lpparam, useGoogle = true) diff --git a/app/src/main/java/io/github/soclear/oneuix/ui/SettingScreen.kt b/app/src/main/java/io/github/soclear/oneuix/ui/SettingScreen.kt index 4ddc2ac..6dfe811 100644 --- a/app/src/main/java/io/github/soclear/oneuix/ui/SettingScreen.kt +++ b/app/src/main/java/io/github/soclear/oneuix/ui/SettingScreen.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.launch import io.github.soclear.oneuix.R import io.github.soclear.oneuix.ui.category.Category import io.github.soclear.oneuix.ui.category.DetailPaneAndroid +import io.github.soclear.oneuix.ui.category.DetailPaneBixby import io.github.soclear.oneuix.ui.category.DetailPaneCall import io.github.soclear.oneuix.ui.category.DetailPaneCamera import io.github.soclear.oneuix.ui.category.DetailPaneOther @@ -30,6 +31,7 @@ import io.github.soclear.oneuix.ui.category.DetailPaneSettings import io.github.soclear.oneuix.ui.category.DetailPaneSystemUI import io.github.soclear.oneuix.ui.category.ListPaneCategory import io.github.soclear.oneuix.ui.category.onAndroidEvent +import io.github.soclear.oneuix.ui.category.onBixbyEvent import io.github.soclear.oneuix.ui.category.onCallEvent import io.github.soclear.oneuix.ui.category.onCameraEvent import io.github.soclear.oneuix.ui.category.onOtherEvent @@ -89,6 +91,11 @@ fun SettingScreen(viewModel: SettingViewModel, modifier: Modifier = Modifier) { onEvent = viewModel::onCameraEvent ) + Category.Bixby -> DetailPaneBixby( + uiState = preference.bixby, + onEvent = viewModel::onBixbyEvent + ) + Category.Other -> DetailPaneOther( uiState = preference.other, onEvent = viewModel::onOtherEvent diff --git a/app/src/main/java/io/github/soclear/oneuix/ui/category/Category.kt b/app/src/main/java/io/github/soclear/oneuix/ui/category/Category.kt index 3216857..745fdf8 100644 --- a/app/src/main/java/io/github/soclear/oneuix/ui/category/Category.kt +++ b/app/src/main/java/io/github/soclear/oneuix/ui/category/Category.kt @@ -9,5 +9,6 @@ enum class Category(val packageName: String) { Settings(Package.SETTINGS), Call(Package.DIALER), Camera(Package.CAMERA), + Bixby(Package.BIXBY_AGENT), Other(BuildConfig.APPLICATION_ID); } diff --git a/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneBixby.kt b/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneBixby.kt new file mode 100644 index 0000000..78883b1 --- /dev/null +++ b/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneBixby.kt @@ -0,0 +1,66 @@ +package io.github.soclear.oneuix.ui.category + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import io.github.soclear.oneuix.R +import io.github.soclear.oneuix.data.Preference +import io.github.soclear.oneuix.ui.SettingViewModel +import io.github.soclear.oneuix.ui.component.SwitchItem + +@Composable +fun DetailPaneBixby( + uiState: Preference.Bixby, + onEvent: (BixbyEvent) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + SwitchItem( + title = stringResource(R.string.bixby_offline_title), + summary = stringResource(R.string.bixby_offline_summary), + icon = ImageVector.vectorResource(R.drawable.wifi_link_speed), + checked = uiState.injectModel, + onCheckedChange = { onEvent(BixbyEvent.InjectModel(it)) }, + ) + SwitchItem( + title = stringResource(R.string.bixby_custom_wakeup_title), + summary = stringResource(R.string.bixby_custom_wakeup_summary), + icon = ImageVector.vectorResource(R.drawable.phone_forwarded), + checked = uiState.labsMgr, + onCheckedChange = { onEvent(BixbyEvent.LabsMgr(it)) }, + ) + SwitchItem( + title = stringResource(R.string.bixby_wwv_bypass_title), + summary = stringResource(R.string.bixby_wwv_bypass_summary), + icon = ImageVector.vectorResource(R.drawable.phone_forwarded), + checked = uiState.wwvBypass, + onCheckedChange = { onEvent(BixbyEvent.WwvBypass(it)) }, + ) + } +} + +sealed interface BixbyEvent { + @JvmInline value class InjectModel(val value: Boolean) : BixbyEvent + @JvmInline value class LabsMgr(val value: Boolean) : BixbyEvent + @JvmInline value class WwvBypass(val value: Boolean) : BixbyEvent +} + +fun SettingViewModel.onBixbyEvent(event: BixbyEvent) { + updateData { preference -> + when (event) { + is BixbyEvent.InjectModel -> preference.copy(bixby = preference.bixby.copy(injectModel = event.value)) + is BixbyEvent.LabsMgr -> preference.copy(bixby = preference.bixby.copy(labsMgr = event.value)) + is BixbyEvent.WwvBypass -> preference.copy(bixby = preference.bixby.copy(wwvBypass = event.value)) + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d91b92..0c649f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -122,4 +122,10 @@ Reboot System UI Reboot to recovery Reboot to download mode + Bixby offline support + Inject device model to bypass Bixby offline restrictions + Custom wakeup phrase + Bypass Labs feature check to enable custom wakeup phrases + Bypass wakeup word blacklist + Bypass native word blacklist validation for custom wakeup words From a951f99958a6d6481349bcbb21a5a3e7b4c89bb2 Mon Sep 17 00:00:00 2001 From: Mzdyl <25149539+Mzdyl@users.noreply.github.com> Date: Wed, 27 May 2026 02:21:32 +0800 Subject: [PATCH 2/2] feat(bixby): update to latest version and add translations - Update Bixby.kt to match Self branch (use Preference.Bixby parameter) - Add Chinese, Russian, French translations for Bixby strings --- .../io/github/soclear/oneuix/hook/Bixby.kt | 322 +++++++----------- .../io/github/soclear/oneuix/hook/Main.kt | 7 +- app/src/main/res/values-fr/strings.xml | 6 + app/src/main/res/values-ru/strings.xml | 6 + app/src/main/res/values-zh/strings.xml | 6 + 5 files changed, 141 insertions(+), 206 deletions(-) diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt b/app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt index 34a8a1c..84464ad 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt @@ -6,6 +6,7 @@ import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers.findAndHookMethod import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam +import io.github.soclear.oneuix.data.Preference import io.github.soclear.oneuix.data.Package import java.io.File import java.lang.reflect.Modifier @@ -13,275 +14,196 @@ import java.util.Locale object Bixby { - fun init(lpparam: LoadPackageParam, injectModel: Boolean, labsMgr: Boolean, wwvBypass: Boolean) { + private fun log(msg: String) { XposedBridge.log("[OneUIX-Bixby] $msg") } + + fun init(lpparam: LoadPackageParam, p: Preference.Bixby) { when (lpparam.packageName) { - Package.BIXBY_AGENT -> initBixbyAgent(lpparam, injectModel, labsMgr, wwvBypass) - Package.BIXBY_WAKEUP -> initBixbyWakeup(lpparam, wwvBypass) + Package.BIXBY_AGENT -> initBixbyAgent(lpparam, p) + Package.BIXBY_WAKEUP -> initBixbyWakeup(lpparam, p) } } - private fun initBixbyAgent( - lpparam: LoadPackageParam, - injectModel: Boolean, - labsMgr: Boolean, - wwvBypass: Boolean, - ) { - XposedBridge.log("[OneUIX-Bixby] Init injectModel=$injectModel labsMgr=$labsMgr wwvBypass=$wwvBypass") - - if (injectModel) { - try { - hookInjectModel(lpparam) - XposedBridge.log("[OneUIX-Bixby] [+] injectModel") - } catch (e: Throwable) { - XposedBridge.log("[OneUIX-Bixby] [-] injectModel: ${e.message}") - } - } - - if (labsMgr) { - try { - hookLabsFeatureManager(lpparam) - XposedBridge.log("[OneUIX-Bixby] [+] labsFeatureManager") - } catch (e: Throwable) { - XposedBridge.log("[OneUIX-Bixby] [-] labsFeatureManager: ${e.message}") - } - } - - if (wwvBypass) { - try { - hookWakeupWordValidator(lpparam) - XposedBridge.log("[OneUIX-Bixby] [+] wakeupWordValidator") - } catch (e: Throwable) { - XposedBridge.log("[OneUIX-Bixby] [-] wakeupWordValidator: ${e.message}") - } - } + private fun initBixbyAgent(lpparam: LoadPackageParam, p: Preference.Bixby) { + log("Init offline=${p.injectModel} customWakeup=${p.labsMgr} wwv=${p.wwvBypass}") + if (p.injectModel) hookInjectModel(lpparam) + if (p.labsMgr) hookLabsFeatureManager(lpparam) + if (p.wwvBypass) hookWakeupWordValidator(lpparam) } - private fun initBixbyWakeup(lpparam: LoadPackageParam, wwvBypass: Boolean) { + private fun initBixbyWakeup(lpparam: LoadPackageParam, p: Preference.Bixby) { hookWakeupCustomPhrase(lpparam) - if (wwvBypass) { + if (p.wwvBypass) { hookWakeupWordTypeValidator(lpparam) hookKwdCjkFix(lpparam) } } + // ═══════ injectModel: 注入 Build.MODEL 到设备白名单缓存 ═══════ + private fun hookInjectModel(lpparam: LoadPackageParam) { - findAndHookMethod( - "android.app.SharedPreferencesImpl", - lpparam.classLoader, - "getString", - String::class.java, - String::class.java, + 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}" - } + override fun beforeHookedMethod(p: MethodHookParam) { + if (p.args[0] == "pref_key_on_device_config_cache") { + val orig = (p.result ?: p.args[1] ?: "") as String + if (!orig.contains(Build.MODEL)) + p.result = if (orig.isEmpty()) Build.MODEL else "$orig,${Build.MODEL}" } } - } - ) + }) } + // ═══════ labsMgr: 绕过 LabsFeatureManager 所有限制 ═══════ + private fun hookLabsFeatureManager(lpparam: LoadPackageParam) { try { - val clazz = Class.forName( - "com.samsung.android.bixby.agent.common.util.datamanager.LabsFeatureManager", - true, - lpparam.classLoader, - ) + val c = Class.forName("com.samsung.android.bixby.agent.common.util.datamanager.LabsFeatureManager", true, lpparam.classLoader) for (name in arrayOf("isSupported", "isAvailable", "isEnabled", "isLabs")) { - findAndHookMethod( - clazz, - name, - String::class.java, - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - if (param.args[0] == "labs_custom_wakeup") { - param.result = true - } - } + findAndHookMethod(c, name, String::class.java, object : XC_MethodHook() { + override fun beforeHookedMethod(mp: MethodHookParam) { + if (mp.args[0] == "labs_custom_wakeup") mp.result = true } - ) + }) } - findAndHookMethod( - clazz, - "isLabsMenuSupported", - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - param.result = true - } - } - ) - } catch (_: Throwable) { - } + findAndHookMethod(c, "isLabsMenuSupported", object : XC_MethodHook() { + override fun beforeHookedMethod(p: MethodHookParam) { p.result = true } + }) + } catch (_: Throwable) {} } + // ═══════ wwvBypass: 绕过原生库唤醒词黑名单(竞品词/脏话/政治等) ═══════ + // 签名匹配而非硬编码方法名,兼容不同 Bixby 版本 + private fun hookWakeupWordValidator(lpparam: LoadPackageParam) { - val clazz = lpparam.classLoader.loadClass("com.samsung.voicewakeup.wwv.WakeupWordValidator") - for (method in clazz.declaredMethods) { - if (!Modifier.isPublic(method.modifiers)) continue - val pts = method.parameterTypes - if (method.returnType == Boolean::class.javaPrimitiveType && - pts.contentEquals(arrayOf(Locale::class.java, String::class.java, String::class.java, String::class.java)) - ) { - XposedBridge.hookMethod( - method, - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - param.result = true - } - } - ) + val cls = lpparam.classLoader.loadClass("com.samsung.voicewakeup.wwv.WakeupWordValidator") + for (m in cls.declaredMethods) { + if (!Modifier.isPublic(m.modifiers)) continue + val pts = m.parameterTypes + // b(Locale, String, String, String) boolean → 绕过长度校验 + if (m.returnType == Boolean::class.javaPrimitiveType && + pts.contentEquals(arrayOf(Locale::class.java, String::class.java, String::class.java, String::class.java))) { + XposedBridge.hookMethod(m, object : XC_MethodHook() { + override fun beforeHookedMethod(p: MethodHookParam) { p.result = true } + }) } - if (method.returnType == Int::class.javaPrimitiveType && - pts.contentEquals(arrayOf(Context::class.java, String::class.java, Locale::class.java, String::class.java)) - ) { - XposedBridge.hookMethod( - method, - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - param.result = 0 - } - } - ) + // d(Context, String, Locale, String) int → 绕过所有黑名单校验 + if (m.returnType == Int::class.javaPrimitiveType && + pts.contentEquals(arrayOf(Context::class.java, String::class.java, Locale::class.java, String::class.java))) { + XposedBridge.hookMethod(m, object : XC_MethodHook() { + override fun beforeHookedMethod(p: MethodHookParam) { p.result = 0 } + }) } } } + // ═══════ wakeup: 绕过 KWV isVaildWordType 的 locale 限制 ═══════ + // zhCN 等非韩语 locale 直接返回 false,导致 TEXT_CUSTOM 训练失败 + // 三个 decoder 变体 (normal/bargein/acousticecho) 均有同名方法 + private fun hookWakeupWordTypeValidator(lpparam: LoadPackageParam) { - for (className in arrayOf( + for (cn in arrayOf( "com.samsung.voicewakeup.kwv.normal.custom.WakeupKwvNormalCommon", "com.samsung.voicewakeup.kwv.bargein.custom.WakeupKwvBargeinCommon", - "com.samsung.voicewakeup.kwv.acousticecho.custom.WakeupKwvAcousticEchoCommon", - )) { + "com.samsung.voicewakeup.kwv.acousticecho.custom.WakeupKwvAcousticEchoCommon")) { try { - val clazz = lpparam.classLoader.loadClass(className) - for (method in clazz.declaredMethods) { - if (method.returnType != Boolean::class.javaPrimitiveType) continue - if (!method.parameterTypes.contentEquals(arrayOf(String::class.java, Locale::class.java))) continue - XposedBridge.hookMethod( - method, - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - param.result = true - } - } - ) + val cls = lpparam.classLoader.loadClass(cn) + for (m in cls.declaredMethods) { + if (m.returnType != Boolean::class.javaPrimitiveType) continue + if (!m.parameterTypes.contentEquals(arrayOf(String::class.java, Locale::class.java))) continue + XposedBridge.hookMethod(m, object : XC_MethodHook() { + override fun beforeHookedMethod(p: MethodHookParam) { p.result = true } + }) } - } catch (_: Throwable) { - } + } catch (_: Throwable) {} } } + // ═══════ wakeup: KWD 引擎强制匹配中文唤醒词 ═══════ + // native KWD 引擎无法处理中文文本,verifyRun 始终返回 0 + // 检测到 mKeyword 含 CJK 字符时强改结果为 1,实际唤醒由 KWV 音频匹配把关 + private fun hookKwdCjkFix(lpparam: LoadPackageParam) { - for (className in arrayOf( + for (kn in arrayOf( "com.samsung.voicewakeup.kwd.normal.custom.WakeupKwdNormalCustom", "com.samsung.voicewakeup.kwd.bargein.custom.WakeupKwdBargeinCustom", - "com.samsung.voicewakeup.kwd.acousticecho.custom.WakeupKwdAcousticEchoCustom", - )) { + "com.samsung.voicewakeup.kwd.acousticecho.custom.WakeupKwdAcousticEchoCustom")) { try { - val clazz = lpparam.classLoader.loadClass(className) + val cls = lpparam.classLoader.loadClass(kn) var kwField: java.lang.reflect.Field? = null - try { - kwField = clazz.getDeclaredField("mKeyword") - kwField.isAccessible = true - } catch (_: Throwable) { - } - - for (method in clazz.declaredMethods) { - if (method.returnType != Int::class.javaPrimitiveType) continue - val pts = method.parameterTypes - val isVr = (pts.size == 1 && pts[0].isArray && pts[0].componentType == Short::class.javaPrimitiveType) || - (pts.size == 3 && pts[0].isArray && pts[0].componentType == Short::class.javaPrimitiveType && - pts[1] == Int::class.javaPrimitiveType && pts[2] == Int::class.javaPrimitiveType) + try { kwField = cls.getDeclaredField("mKeyword"); kwField.isAccessible = true } catch (_: Throwable) {} + + for (m in cls.declaredMethods) { + if (m.returnType != Int::class.javaPrimitiveType) continue + val pts = m.parameterTypes + val isVr = (pts.size == 1 && pts[0].isArray && pts[0].componentType == Short::class.javaPrimitiveType) + || (pts.size == 3 && pts[0].isArray && pts[0].componentType == Short::class.javaPrimitiveType + && pts[1] == Int::class.javaPrimitiveType && pts[2] == Int::class.javaPrimitiveType) if (!isVr) continue - val kwRef = kwField - XposedBridge.hookMethod( - method, - object : XC_MethodHook() { - override fun afterHookedMethod(param: MethodHookParam) { - val ret = param.result as? Int ?: return - if (ret != 0) return - val kw = try { - kwRef?.get(param.thisObject) ?: "" - } catch (_: Throwable) { - "" - } - if ((kw as? String)?.any { it in '\u4E00'..'\u9FFF' } == true) { - param.result = 1 - } - } + val kwF = kwField + XposedBridge.hookMethod(m, object : XC_MethodHook() { + override fun afterHookedMethod(p: MethodHookParam) { + val ret = p.result as? Int ?: return + if (ret != 0) return + val kw = try { kwF?.get(p.thisObject) ?: "" } catch (_: Throwable) { "" } + if ((kw as? String)?.any { it in '\u4E00'..'\u9FFF' } == true) + p.result = 1 } - ) + }) } - } catch (_: Throwable) { - } + } catch (_: Throwable) {} } } + // ═══════ wakeup: 修复自定义短语文本返回空的问题 ═══════ + private fun hookWakeupCustomPhrase(lpparam: LoadPackageParam) { - findAndHookMethod( - "android.app.SharedPreferencesImpl", - lpparam.classLoader, - "getString", - String::class.java, - String::class.java, + // SP.getString → 数据源为空时从 XML 文件读取 + 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] ?: "" + override fun beforeHookedMethod(p: MethodHookParam) { + if (p.args[0] == "myvoice_string_custom") { + val orig = p.result ?: p.args[1] ?: "" if (orig.toString().isEmpty()) { val txt = readWakeupSP("myvoice_string_custom") - if (txt.isNotEmpty()) param.result = txt + if (txt.isNotEmpty()) p.result = txt } } } - } - ) + }) + // MatrixCursor.addRow → ContentProvider locale 不匹配后丢弃文本 try { - val cursorClass = lpparam.classLoader.loadClass("android.database.MatrixCursor") - findAndHookMethod( - cursorClass, - "addRow", - arrayOfNulls(0).javaClass, - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - val row = param.args[0] as? Array ?: return - try { - val cols = param.thisObject.javaClass - .getDeclaredField("columnNames") - .apply { isAccessible = true } - .get(param.thisObject) as? Array ?: 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 - } + val c = lpparam.classLoader.loadClass("android.database.MatrixCursor") + findAndHookMethod(c, "addRow", arrayOfNulls(0).javaClass, object : XC_MethodHook() { + override fun beforeHookedMethod(p: MethodHookParam) { + val row = p.args[0] as? Array ?: return + try { + val cols = p.thisObject.javaClass.getDeclaredField("columnNames").apply { isAccessible = true }.get(p.thisObject) as? Array ?: 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) {} } - ) - } 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("(.*?)").find(file.readText()) - if (match != null) return match.groupValues[1] + for (f in dir.listFiles() ?: emptyArray()) { + if (!f.name.endsWith(".xml")) continue + val m = Regex("(.*?)").find(f.readText()) + if (m != null) return m.groupValues[1] } - } catch (_: Throwable) { - } + } catch (_: Throwable) {} return "" } } diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt b/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt index 06da347..de2f920 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt @@ -391,12 +391,7 @@ class Main : IXposedHookLoadPackage, IXposedHookInitPackageResources, IXposedHoo Package.BIXBY_AGENT, Package.BIXBY_WAKEUP -> { if (preference.bixby.injectModel || preference.bixby.labsMgr || preference.bixby.wwvBypass) { - Bixby.init( - lpparam = lpparam, - injectModel = preference.bixby.injectModel, - labsMgr = preference.bixby.labsMgr, - wwvBypass = preference.bixby.wwvBypass, - ) + Bixby.init(lpparam, preference.bixby) } } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a05ea19..6137bd8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -122,4 +122,10 @@ Redémarrer l\'UI système Redémarrer en Recovery Redémarrer en mode Download + Support hors ligne de Bixby + Injecter le modèle de l\'appareil pour contourner les restrictions hors ligne de Bixby + Phrase de réveil personnalisée + Contourner la vérification Labs pour activer les phrases de réveil personnalisées + Contourner la liste noire des mots de réveil + Contourner la validation de la liste noire native pour les mots de réveil personnalisés diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a4df224..6bd6151 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -122,4 +122,10 @@ Перезапуск System UI Перезагрузка в Recovery Перезагрузка в режим Download + Офлайн-поддержка Bixby + Внедрить модель устройства для обхода офлайн-ограничений Bixby + Пользовательская фраза пробуждения + Обойти проверку функции Labs для включения пользовательских фраз пробуждения + Обход чёрного списка слов пробуждения + Обойти проверку чёрного списка нативных слов для пользовательских слов пробуждения diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 1a94943..356bfd6 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -122,4 +122,10 @@ 重启系统 UI 重启到 Recovery 重启到下载模式 + Bixby 离线支持 + 注入设备型号以绕过 Bixby 离线限制 + 自定义唤醒短语 + 绕过 Labs 功能检查以启用自定义唤醒短语 + 绕过唤醒词黑名单 + 绕过原生词库黑名单验证,支持自定义唤醒词