diff --git a/README.md b/README.md index 3f87e711..f51bae4d 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,9 @@ - 禁用图片编辑器 AI 水印 - 绕过 Samsung Health Monitor 国家检查 - S Pen 使用谷歌翻译 +- 解锁 Bixby 离线处理 +- 支持 Bixby 自定义唤醒词 +- 绕过 Bixby 唤醒词限制 - 隐藏应用屏幕搜索栏 - 移除快捷方式图标右下角小角标 - 手表连接模式(WearOS CN / WearOS Global) @@ -239,6 +242,9 @@ - Disable Photo Editor AI watermark - Bypass Samsung Health Monitor country check - Use Google Translate for S Pen +- Unlock Bixby offline processing +- Support Bixby custom wake words +- Bypass Bixby wake word restrictions - Hide search bar on app screen - Remove bottom-right shortcut badge - Watch connection mode (WearOS CN / WearOS Global) 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 277607ef..f64562c3 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 @@ -3,6 +3,8 @@ package io.github.soclear.oneuix.data object Package { const val ANDROID = "android" const val BROWSER = "com.sec.android.app.sbrowser" + const val BIXBY_AGENT = "com.samsung.android.bixby.agent" + const val BIXBY_WAKEUP = "com.samsung.android.bixby.wakeup" const val CALENDAR = "com.samsung.android.calendar" const val CAMERA = "com.sec.android.app.camera" const val DIALER = "com.samsung.android.dialer" 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 e831e490..033e8ed0 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 @@ -132,6 +132,9 @@ data class Preference( val noAIWatermark: Boolean = true, val bypassHealthMonitorCountryCheck: Boolean = false, val useSPenGoogleTranslate: Boolean = false, + val unlockBixbyOfflineProcessing: Boolean = false, + val supportBixbyCustomWakeup: Boolean = false, + val bypassBixbyWakeWordRestrictions: Boolean = false, val hideAppsSearchBar: Boolean = false, val removeShortcutBadge: Boolean = false, val bypassWatchPairingRegionCheck: Boolean = false, 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 00000000..5d24e114 --- /dev/null +++ b/app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt @@ -0,0 +1,391 @@ +package io.github.soclear.oneuix.hook + +import android.content.Context +import android.os.Build +import android.os.SystemClock +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.XposedHelpers.findClass +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, + unlockOfflineProcessing: Boolean, + supportCustomWakeup: Boolean, + bypassWakeWordRestrictions: Boolean, + ) { + when (lpparam.packageName) { + Package.BIXBY_AGENT -> { + if (unlockOfflineProcessing) { + unlockOfflineProcessing(lpparam) + } + if (supportCustomWakeup) { + supportCustomWakeup(lpparam) + } + if (bypassWakeWordRestrictions) { + bypassWakeWordRestrictions(lpparam) + } + } + + Package.BIXBY_WAKEUP -> { + if (supportCustomWakeup || bypassWakeWordRestrictions) { + restoreCustomWakeupText(lpparam) + } + if (bypassWakeWordRestrictions) { + allowWakeupWordTypes(lpparam) + fixAsianKeywordDetection(lpparam) + } + } + } + } + + private fun unlockOfflineProcessing(lpparam: LoadPackageParam) { + try { + 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] != DEVICE_CONFIG_CACHE_KEY) return + + val original = param.result as? String + val defaultValue = param.args[1] as? String + val cache = original ?: defaultValue.orEmpty() + val deviceModel = Build.MODEL + + if (deviceModel.isBlank() || cache.split(",").contains(deviceModel)) { + return + } + param.result = if (cache.isBlank()) { + deviceModel + } else { + "$cache,$deviceModel" + } + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + + private fun supportCustomWakeup(lpparam: LoadPackageParam) { + val labsFeatureManagerClass = try { + findClass(LABS_FEATURE_MANAGER_CLASS, lpparam.classLoader) + } catch (t: Throwable) { + XposedBridge.log(t) + return + } + + val featureCallback = object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + if (param.args[0] == CUSTOM_WAKEUP_FEATURE) { + param.result = true + } + } + } + + listOf("isSupported", "isAvailable", "isEnabled", "isLabs").forEach { methodName -> + try { + findAndHookMethod( + labsFeatureManagerClass, + methodName, + String::class.java, + featureCallback + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + + try { + findAndHookMethod( + labsFeatureManagerClass, + "isLabsMenuSupported", + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + param.result = true + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + + private fun bypassWakeWordRestrictions(lpparam: LoadPackageParam) { + val wakeupWordValidatorClass = try { + findClass(WAKEUP_WORD_VALIDATOR_CLASS, lpparam.classLoader) + } catch (t: Throwable) { + XposedBridge.log(t) + return + } + + wakeupWordValidatorClass.declaredMethods.forEach { method -> + if (!Modifier.isPublic(method.modifiers)) return@forEach + + val parameterTypes = method.parameterTypes + if (method.returnType == Boolean::class.javaPrimitiveType && + parameterTypes.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 && + parameterTypes.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 restoreCustomWakeupText(lpparam: LoadPackageParam) { + try { + 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] != CUSTOM_WAKEUP_TEXT_KEY) return + if (!(param.result as? String).isNullOrEmpty()) return + + val text = readCustomWakeupText() + if (text.isNotEmpty()) { + param.result = text + } + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + + try { + val matrixCursorClass = findClass("android.database.MatrixCursor", lpparam.classLoader) + val columnNamesField = try { + matrixCursorClass + .getDeclaredField("columnNames") + .apply { isAccessible = true } + } catch (t: Throwable) { + XposedBridge.log(t) + null + } + findAndHookMethod( + matrixCursorClass, + "addRow", + arrayOfNulls(0).javaClass, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + val row = param.args[0] as? Array<*> ?: return + val columnNames = try { + columnNamesField?.get(param.thisObject) as? Array<*> ?: return + } catch (_: Throwable) { + return + } + + columnNames.forEachIndexed { index, columnName -> + if (columnName != "customKeyword") return@forEachIndexed + if (!(row[index] as? String).isNullOrEmpty()) return@forEachIndexed + + val text = readCustomWakeupText() + if (text.isNotEmpty()) { + @Suppress("UNCHECKED_CAST") + (row as Array)[index] = text + } + } + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + + private fun allowWakeupWordTypes(lpparam: LoadPackageParam) { + WAKEUP_KWV_CLASSES.forEach { className -> + try { + val clazz = findClass(className, lpparam.classLoader) + clazz.declaredMethods.forEach { method -> + if (method.returnType != Boolean::class.javaPrimitiveType) return@forEach + if (!method.parameterTypes.contentEquals(arrayOf(String::class.java, Locale::class.java))) { + return@forEach + } + + XposedBridge.hookMethod( + method, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + param.result = true + } + } + ) + } + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + } + + private fun fixAsianKeywordDetection(lpparam: LoadPackageParam) { + WAKEUP_KWD_CLASSES.forEach { className -> + try { + val clazz = findClass(className, lpparam.classLoader) + val keywordField = try { + clazz.getDeclaredField("mKeyword").apply { isAccessible = true } + } catch (_: Throwable) { + null + } + + clazz.declaredMethods.forEach { method -> + if (method.returnType != Int::class.javaPrimitiveType) return@forEach + if (!method.hasKwdVerifyRunSignature()) return@forEach + + XposedBridge.hookMethod( + method, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + if (param.result != 0) return + + val keyword = try { + keywordField?.get(param.thisObject) as? String + } catch (_: Throwable) { + null + } ?: return + + if (keyword.any { it.isAsianWakeupCharacter() }) { + param.result = 1 + } + } + } + ) + } + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + } + + private fun Char.isAsianWakeupCharacter(): Boolean { + return this in '\u4E00'..'\u9FFF' || + this in '\uAC00'..'\uD7AF' || + this in '\u3040'..'\u30FF' + } + + private fun java.lang.reflect.Method.hasKwdVerifyRunSignature(): Boolean { + val types = parameterTypes + val hasShortArray = types.firstOrNull()?.let { + it.isArray && it.componentType == Short::class.javaPrimitiveType + } == true + + return when (types.size) { + 1 -> hasShortArray + 3 -> hasShortArray && + types[1] == Int::class.javaPrimitiveType && + types[2] == Int::class.javaPrimitiveType + + else -> false + } + } + + private fun readCustomWakeupText(): String { + val now = SystemClock.elapsedRealtime() + val cachedText = cachedCustomWakeupText + if (cachedText != null && + now - cachedCustomWakeupTextTimeMillis < CUSTOM_WAKEUP_TEXT_CACHE_TTL_MILLIS + ) { + return cachedText + } + + val text = try { + val directory = File(BIXBY_WAKEUP_SHARED_PREFERENCES_PATH) + if (!directory.isDirectory) { + "" + } else { + directory.listFiles() + ?.asSequence() + ?.filter { it.name.endsWith(".xml") } + ?.mapNotNull { file -> + customWakeupTextRegex + .find(file.readText()) + ?.groupValues + ?.get(1) + } + ?.firstOrNull() + .orEmpty() + } + } catch (_: Throwable) { + "" + } + cachedCustomWakeupText = text + cachedCustomWakeupTextTimeMillis = now + return text + } + + private const val DEVICE_CONFIG_CACHE_KEY = "pref_key_on_device_config_cache" + private const val CUSTOM_WAKEUP_FEATURE = "labs_custom_wakeup" + private const val CUSTOM_WAKEUP_TEXT_KEY = "myvoice_string_custom" + private const val CUSTOM_WAKEUP_TEXT_CACHE_TTL_MILLIS = 5_000L + private const val BIXBY_WAKEUP_SHARED_PREFERENCES_PATH = + "/data/data/com.samsung.android.bixby.wakeup/shared_prefs" + private const val LABS_FEATURE_MANAGER_CLASS = + "com.samsung.android.bixby.agent.common.util.datamanager.LabsFeatureManager" + private const val WAKEUP_WORD_VALIDATOR_CLASS = + "com.samsung.voicewakeup.wwv.WakeupWordValidator" + private val WAKEUP_KWV_CLASSES = arrayOf( + "com.samsung.voicewakeup.kwv.normal.custom.WakeupKwvNormalCommon", + "com.samsung.voicewakeup.kwv.bargein.custom.WakeupKwvBargeinCommon", + "com.samsung.voicewakeup.kwv.acousticecho.custom.WakeupKwvAcousticEchoCommon", + ) + private val WAKEUP_KWD_CLASSES = arrayOf( + "com.samsung.voicewakeup.kwd.normal.custom.WakeupKwdNormalCustom", + "com.samsung.voicewakeup.kwd.bargein.custom.WakeupKwdBargeinCustom", + "com.samsung.voicewakeup.kwd.acousticecho.custom.WakeupKwdAcousticEchoCustom", + ) + private val customWakeupTextRegex by lazy { + Regex("(.*?)") + } + @Volatile + private var cachedCustomWakeupText: String? = null + @Volatile + private var cachedCustomWakeupTextTimeMillis: Long = 0 +} 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 8676173d..8066c752 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 @@ -58,6 +58,21 @@ class Main : IXposedHookLoadPackage, IXposedHookInitPackageResources, IXposedHoo } } + Package.BIXBY_AGENT, + Package.BIXBY_WAKEUP -> { + if (preference.other.unlockBixbyOfflineProcessing || + preference.other.supportBixbyCustomWakeup || + preference.other.bypassBixbyWakeWordRestrictions + ) { + Bixby.init( + lpparam = lpparam, + unlockOfflineProcessing = preference.other.unlockBixbyOfflineProcessing, + supportCustomWakeup = preference.other.supportBixbyCustomWakeup, + bypassWakeWordRestrictions = preference.other.bypassBixbyWakeWordRestrictions + ) + } + } + Package.CALENDAR -> { if (preference.other.enableChineseHolidayDisplay) { Calendar.enableChineseHolidayDisplay(lpparam) diff --git a/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneOther.kt b/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneOther.kt index b0f55b66..05e67947 100644 --- a/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneOther.kt +++ b/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneOther.kt @@ -122,6 +122,27 @@ fun DetailPaneOther( checked = uiState.useSPenGoogleTranslate, onCheckedChange = { onEvent(OtherEvent.UseSPenGoogleTranslate(it)) } ) + SwitchItem( + icon = ImageVector.vectorResource(id = R.drawable.wifi_link_speed), + title = stringResource(id = R.string.unlockBixbyOfflineProcessing_title), + summary = stringResource(id = R.string.unlockBixbyOfflineProcessing_summary), + checked = uiState.unlockBixbyOfflineProcessing, + onCheckedChange = { onEvent(OtherEvent.UnlockBixbyOfflineProcessing(it)) } + ) + SwitchItem( + icon = ImageVector.vectorResource(id = R.drawable.phone_forwarded), + title = stringResource(id = R.string.supportBixbyCustomWakeup_title), + summary = stringResource(id = R.string.supportBixbyCustomWakeup_summary), + checked = uiState.supportBixbyCustomWakeup, + onCheckedChange = { onEvent(OtherEvent.SupportBixbyCustomWakeup(it)) } + ) + SwitchItem( + icon = ImageVector.vectorResource(id = R.drawable.lock_open), + title = stringResource(id = R.string.bypassBixbyWakeWordRestrictions_title), + summary = stringResource(id = R.string.bypassBixbyWakeWordRestrictions_summary), + checked = uiState.bypassBixbyWakeWordRestrictions, + onCheckedChange = { onEvent(OtherEvent.BypassBixbyWakeWordRestrictions(it)) } + ) SwitchItem( icon = ImageVector.vectorResource(id = R.drawable.apps), title = stringResource(id = R.string.hideAppsSearchBar_title), @@ -210,6 +231,15 @@ sealed interface OtherEvent { @JvmInline value class UseSPenGoogleTranslate(val value: Boolean) : OtherEvent + @JvmInline + value class UnlockBixbyOfflineProcessing(val value: Boolean) : OtherEvent + + @JvmInline + value class SupportBixbyCustomWakeup(val value: Boolean) : OtherEvent + + @JvmInline + value class BypassBixbyWakeWordRestrictions(val value: Boolean) : OtherEvent + @JvmInline value class HideAppsSearchBar(val value: Boolean) : OtherEvent @@ -313,6 +343,24 @@ fun SettingViewModel.onOtherEvent(event: OtherEvent) { ) ) + is OtherEvent.UnlockBixbyOfflineProcessing -> preference.copy( + other = preference.other.copy( + unlockBixbyOfflineProcessing = event.value + ) + ) + + is OtherEvent.SupportBixbyCustomWakeup -> preference.copy( + other = preference.other.copy( + supportBixbyCustomWakeup = event.value + ) + ) + + is OtherEvent.BypassBixbyWakeWordRestrictions -> preference.copy( + other = preference.other.copy( + bypassBixbyWakeWordRestrictions = event.value + ) + ) + is OtherEvent.HideAppsSearchBar -> preference.copy( other = preference.other.copy( hideAppsSearchBar = event.value diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 87d69b3b..d52f1dd1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -110,6 +110,12 @@ Utiliser Health Monitor dans les pays non pris en charge Utiliser Google Traduction pour S Pen Basculer la source de traduction S Pen de Baidu à Google + Déverrouiller le traitement hors ligne de Bixby + Ajouter ce modèle d\'appareil à la liste d\'autorisation de Bixby pour activer le traitement hors ligne et les conversations mains libres + Prendre en charge les mots de réveil Bixby personnalisés + Contourner les vérifications Labs et corriger le texte vide des mots de réveil personnalisés + Contourner les restrictions des mots de réveil Bixby + Contourner les vérifications du texte des mots de réveil pour éviter le blocage par les validateurs natifs Prend en charge les Appels/SMS sur les autres appareils Paramètres > Appareils connectés > Appels/SMS sur les autres appareils Définir le nom de l\'opérateur personnalisé diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4b07dad8..e0b4ad5c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -110,6 +110,12 @@ Разрешить использование Health Monitor в неподдерживаемых странах Использовать Google Переводчик для S Pen Переключить источник перевода S Pen с Baidu на Google + Разблокировать автономную обработку Bixby + Добавить модель этого устройства в список разрешенных Bixby, чтобы включить автономную обработку и диалоги без фразы пробуждения + Поддержка пользовательских слов пробуждения Bixby + Обойти проверки Labs и исправить пустой текст пользовательского слова пробуждения + Обход ограничений слов пробуждения Bixby + Обойти проверки текста слов пробуждения, чтобы пользовательские слова не блокировались нативными валидаторами Вызовы и СМС на других устройствах Настройки -> Подключенные устройства -> Вызовы и СМС на других устройствах Собственное имя оператора diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 85cb3c32..a7c7680d 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -110,6 +110,12 @@ 在不支持的国家/地区使用 Health Monitor S Pen 使用谷歌翻译 将 S Pen 翻译源从百度切换为谷歌 + 解锁 Bixby 离线处理 + 将本机型号加入 Bixby 白名单,以启用离线处理和免唤醒对话 + 支持 Bixby 自定义唤醒词 + 绕过 Labs 检查,并修复自定义唤醒词文本为空的问题 + 绕过 Bixby 唤醒词限制 + 绕过唤醒词文本校验,避免自定义唤醒词被原生校验器拦截 支持跨设备接打电话和收发短信 设置 -> 已连接的设备 -> 跨设备接打电话和收发短信 自定义运营商名称 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 2ee49831..fd02da7b 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -7,6 +7,8 @@ com.android.systemui com.samsung.android.app.notes com.samsung.android.app.telephonyui + com.samsung.android.bixby.agent + com.samsung.android.bixby.wakeup com.samsung.android.calendar com.samsung.android.da.daagent com.samsung.android.dialer diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2114ceda..234b7770 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -110,6 +110,12 @@ Allow using Health Monitor in unsupported countries Use Google Translate for S Pen Switch S Pen translate source from Baidu to Google + Unlock Bixby offline processing + Add this device model to Bixby\'s allowlist to enable offline processing and hands-free conversations + Support Bixby custom wake words + Bypass Labs checks and fix empty custom wake word text + Bypass Bixby wake word restrictions + Bypass wake word text checks so custom wake words are not blocked by native validators Support call & text on other devices Settings -> Connected devices -> Call & text on other devices Set custom carrier name