Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/main/java/io/github/soclear/oneuix/data/Package.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/io/github/soclear/oneuix/data/Preference.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
209 changes: 209 additions & 0 deletions app/src/main/java/io/github/soclear/oneuix/hook/Bixby.kt
Original file line number Diff line number Diff line change
@@ -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}"
}
}
})
}
Comment on lines +43 to +55

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


// ═══════ 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<Any>(0).javaClass, object : XC_MethodHook() {
override fun beforeHookedMethod(p: MethodHookParam) {
val row = p.args[0] as? Array<Any?> ?: return
try {
val cols = p.thisObject.javaClass.getDeclaredField("columnNames").apply { isAccessible = true }.get(p.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 +161 to +195

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) {
        }
    }


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("<string name=\"$key\">(.*?)</string>").find(f.readText())
if (m != null) return m.groupValues[1]
}
} catch (_: Throwable) {}
return ""
}
}
Comment on lines +197 to +209

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

6 changes: 6 additions & 0 deletions app/src/main/java/io/github/soclear/oneuix/hook/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ 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
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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,10 @@
<string name="restartSystemUI">Redémarrer l\'UI système</string>
<string name="restartRecovery">Redémarrer en Recovery</string>
<string name="restartDownload">Redémarrer en mode Download</string>
<string name="bixby_offline_title">Support hors ligne de Bixby</string>
<string name="bixby_offline_summary">Injecter le modèle de l\'appareil pour contourner les restrictions hors ligne de Bixby</string>
<string name="bixby_custom_wakeup_title">Phrase de réveil personnalisée</string>
<string name="bixby_custom_wakeup_summary">Contourner la vérification Labs pour activer les phrases de réveil personnalisées</string>
<string name="bixby_wwv_bypass_title">Contourner la liste noire des mots de réveil</string>
<string name="bixby_wwv_bypass_summary">Contourner la validation de la liste noire native pour les mots de réveil personnalisés</string>
</resources>
6 changes: 6 additions & 0 deletions app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,10 @@
<string name="restartSystemUI">Перезапуск System UI</string>
<string name="restartRecovery">Перезагрузка в Recovery</string>
<string name="restartDownload">Перезагрузка в режим Download</string>
<string name="bixby_offline_title">Офлайн-поддержка Bixby</string>
<string name="bixby_offline_summary">Внедрить модель устройства для обхода офлайн-ограничений Bixby</string>
<string name="bixby_custom_wakeup_title">Пользовательская фраза пробуждения</string>
<string name="bixby_custom_wakeup_summary">Обойти проверку функции Labs для включения пользовательских фраз пробуждения</string>
<string name="bixby_wwv_bypass_title">Обход чёрного списка слов пробуждения</string>
<string name="bixby_wwv_bypass_summary">Обойти проверку чёрного списка нативных слов для пользовательских слов пробуждения</string>
</resources>
6 changes: 6 additions & 0 deletions app/src/main/res/values-zh/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,10 @@
<string name="restartSystemUI">重启系统 UI</string>
<string name="restartRecovery">重启到 Recovery</string>
<string name="restartDownload">重启到下载模式</string>
<string name="bixby_offline_title">Bixby 离线支持</string>
<string name="bixby_offline_summary">注入设备型号以绕过 Bixby 离线限制</string>
<string name="bixby_custom_wakeup_title">自定义唤醒短语</string>
<string name="bixby_custom_wakeup_summary">绕过 Labs 功能检查以启用自定义唤醒短语</string>
<string name="bixby_wwv_bypass_title">绕过唤醒词黑名单</string>
<string name="bixby_wwv_bypass_summary">绕过原生词库黑名单验证,支持自定义唤醒词</string>
</resources>
Loading