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..84464ad --- /dev/null +++ b/app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt @@ -0,0 +1,209 @@ +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.Preference +import io.github.soclear.oneuix.data.Package +import java.io.File +import java.lang.reflect.Modifier +import java.util.Locale + +object Bixby { + + 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, p) + Package.BIXBY_WAKEUP -> initBixbyWakeup(lpparam, p) + } + } + + 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, p: Preference.Bixby) { + hookWakeupCustomPhrase(lpparam) + 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, + object : XC_MethodHook() { + 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 c = Class.forName("com.samsung.android.bixby.agent.common.util.datamanager.LabsFeatureManager", true, lpparam.classLoader) + for (name in arrayOf("isSupported", "isAvailable", "isEnabled", "isLabs")) { + 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(c, "isLabsMenuSupported", object : XC_MethodHook() { + override fun beforeHookedMethod(p: MethodHookParam) { p.result = true } + }) + } catch (_: Throwable) {} + } + + // ═══════ wwvBypass: 绕过原生库唤醒词黑名单(竞品词/脏话/政治等) ═══════ + // 签名匹配而非硬编码方法名,兼容不同 Bixby 版本 + + private fun hookWakeupWordValidator(lpparam: LoadPackageParam) { + 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 } + }) + } + // 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 (cn 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 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) {} + } + } + + // ═══════ wakeup: KWD 引擎强制匹配中文唤醒词 ═══════ + // native KWD 引擎无法处理中文文本,verifyRun 始终返回 0 + // 检测到 mKeyword 含 CJK 字符时强改结果为 1,实际唤醒由 KWV 音频匹配把关 + + private fun hookKwdCjkFix(lpparam: LoadPackageParam) { + 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")) { + try { + val cls = lpparam.classLoader.loadClass(kn) + var kwField: java.lang.reflect.Field? = null + 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 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) {} + } + } + + // ═══════ wakeup: 修复自定义短语文本返回空的问题 ═══════ + + private fun hookWakeupCustomPhrase(lpparam: LoadPackageParam) { + // SP.getString → 数据源为空时从 XML 文件读取 + findAndHookMethod("android.app.SharedPreferencesImpl", lpparam.classLoader, + "getString", String::class.java, String::class.java, + object : XC_MethodHook() { + 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()) p.result = txt + } + } + } + }) + // MatrixCursor.addRow → ContentProvider locale 不匹配后丢弃文本 + try { + 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) {} + } + + 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 (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) {} + 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..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 @@ -389,6 +389,12 @@ class Main : IXposedHookLoadPackage, IXposedHookInitPackageResources, IXposedHoo } } + Package.BIXBY_AGENT, Package.BIXBY_WAKEUP -> { + if (preference.bixby.injectModel || preference.bixby.labsMgr || preference.bixby.wwvBypass) { + Bixby.init(lpparam, preference.bixby) + } + } + "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-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 功能检查以启用自定义唤醒短语 + 绕过唤醒词黑名单 + 绕过原生词库黑名单验证,支持自定义唤醒词 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