From ff54faadb61ac53f9aeee9029878b9fed41f1de9 Mon Sep 17 00:00:00 2001 From: Mzdyl <25149539+Mzdyl@users.noreply.github.com> Date: Tue, 12 May 2026 16:54:53 +0800 Subject: [PATCH 1/2] feat: add watch pairing tweaks --- .../io/github/soclear/oneuix/data/Package.kt | 1 + .../github/soclear/oneuix/data/Preference.kt | 3 + .../io/github/soclear/oneuix/hook/Main.kt | 13 ++ .../soclear/oneuix/hook/WatchPairing.kt | 164 ++++++++++++++++++ .../oneuix/ui/category/DetailPaneOther.kt | 63 +++++++ .../soclear/oneuix/ui/component/SelectItem.kt | 61 +++++++ app/src/main/res/drawable/google_play.xml | 9 + app/src/main/res/drawable/lock_open.xml | 9 + app/src/main/res/drawable/watch_pairing.xml | 9 + app/src/main/res/values-fr/strings.xml | 9 + app/src/main/res/values-ru/strings.xml | 9 + app/src/main/res/values-zh/strings.xml | 9 + app/src/main/res/values/arrays.xml | 1 + app/src/main/res/values/strings.xml | 9 + 14 files changed, 369 insertions(+) create mode 100644 app/src/main/java/io/github/soclear/oneuix/hook/WatchPairing.kt create mode 100644 app/src/main/java/io/github/soclear/oneuix/ui/component/SelectItem.kt create mode 100644 app/src/main/res/drawable/google_play.xml create mode 100644 app/src/main/res/drawable/lock_open.xml create mode 100644 app/src/main/res/drawable/watch_pairing.xml 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 6cd940b..277607e 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 @@ -22,4 +22,5 @@ object Package { const val THEME_CENTER = "com.samsung.android.themecenter" const val WEATHER = "com.sec.android.daemonapp" const val VIDEO = "com.samsung.android.video" + const val WATCH_MANAGER = "com.samsung.android.app.watchmanager" } 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 cad68c7..3688b22 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 @@ -126,6 +126,9 @@ data class Preference( val bypassHealthMonitorCountryCheck: Boolean = false, val useSPenGoogleTranslate: Boolean = false, val hideAppsSearchBar: Boolean = false, + val bypassWatchPairingRegionCheck: Boolean = false, + val watchPairingConnectionMode: Int = 0, + val supplementChinaWearOsGms: Boolean = false, ) companion object { 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 8bfe54b..862cfac 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 @@ -353,6 +353,19 @@ class Main : IXposedHookLoadPackage, IXposedHookInitPackageResources { } } + Package.WATCH_MANAGER -> { + if (preference.other.bypassWatchPairingRegionCheck || + preference.other.watchPairingConnectionMode != WatchPairing.MODE_NONE + ) { + WatchPairing.init( + lpparam = lpparam, + bypassRegionCheck = preference.other.bypassWatchPairingRegionCheck, + connectionMode = preference.other.watchPairingConnectionMode, + supplementChinaWearOsGms = preference.other.supplementChinaWearOsGms + ) + } + } + "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/hook/WatchPairing.kt b/app/src/main/java/io/github/soclear/oneuix/hook/WatchPairing.kt new file mode 100644 index 0000000..be881a5 --- /dev/null +++ b/app/src/main/java/io/github/soclear/oneuix/hook/WatchPairing.kt @@ -0,0 +1,164 @@ +package io.github.soclear.oneuix.hook + +import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XC_MethodReplacement +import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedHelpers +import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam +import io.github.soclear.oneuix.data.Package + +object WatchPairing { + const val MODE_NONE = 0 + const val MODE_WEAROS_CN = 1 + const val MODE_WEAROS_GLOBAL = 2 + + fun init( + lpparam: LoadPackageParam, + bypassRegionCheck: Boolean, + connectionMode: Int, + supplementChinaWearOsGms: Boolean, + ) { + if (lpparam.packageName != Package.WATCH_MANAGER) return + + XposedBridge.log("OneUIX: WatchPairing init: bypassRegionCheck=$bypassRegionCheck, connectionMode=$connectionMode") + + if (bypassRegionCheck) { + bypassRegionCheck(lpparam) + disableCscCheck(lpparam) + bypassPairingProblemCheck(lpparam) + spoofCscValue(lpparam) + } + + if (connectionMode != MODE_NONE) { + spoofChinaEdition(lpparam, connectionMode) + } + + if (connectionMode == MODE_WEAROS_CN && supplementChinaWearOsGms) { + supplementChinaWearOsGms(lpparam) + } + } + + private fun bypassRegionCheck(lpparam: LoadPackageParam) { + try { + XposedHelpers.findAndHookMethod( + "com.samsung.android.app.twatchmanager.connectionmanager.util.BluetoothUuidUtil", + lpparam.classLoader, + "checkDeviceRegion", + android.content.Context::class.java, + android.bluetooth.BluetoothDevice::class.java, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + param.result = false + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + + private fun spoofChinaEdition(lpparam: LoadPackageParam, connectionMode: Int) { + try { + XposedHelpers.findAndHookMethod( + "com.samsung.android.app.global.utils.GoogleRequirementUtils", + lpparam.classLoader, + "isChinaEdition", + android.content.Context::class.java, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + when (connectionMode) { + MODE_WEAROS_CN -> param.result = true + MODE_WEAROS_GLOBAL -> param.result = false + } + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + + private fun disableCscCheck(lpparam: LoadPackageParam) { + try { + XposedHelpers.findAndHookMethod( + "com.samsung.android.app.global.utils.PlatformUtils", + lpparam.classLoader, + "isSamsungChinaModel", + object : XC_MethodReplacement() { + override fun replaceHookedMethod(param: MethodHookParam): Any { + return false + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + + private fun bypassPairingProblemCheck(lpparam: LoadPackageParam) { + try { + val problemClass = XposedHelpers.findClass( + "com.samsung.android.app.watchmanager.setupwizard.pairing.PairingProblemChecker\$Problem", + lpparam.classLoader + ) + + XposedHelpers.findAndHookMethod( + "com.samsung.android.app.watchmanager.setupwizard.pairing.PairingProblemChecker", + lpparam.classLoader, + "problemCheckAfterPairing", + "com.samsung.android.app.twatchmanager.connectionmanager.define.WearableDevice", + "android.bluetooth.BluetoothDevice", + Boolean::class.javaPrimitiveType, + "androidx.fragment.app.FragmentActivity", + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + if (param.result?.toString() == "WEAR_OS_NOT_SUPPORTED_PHONE") { + param.result = XposedHelpers.getStaticObjectField( + problemClass, + "NO_PROBLEM" + ) + } + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + + private fun supplementChinaWearOsGms(lpparam: LoadPackageParam) { + try { + XposedHelpers.findAndHookMethod( + "com.samsung.android.app.watchmanager.setupwizard.downloadinstall.HMConnectFragment", + lpparam.classLoader, + "makePackageListToDownload", + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + val packages = param.result as? MutableSet<*> ?: return + @Suppress("UNCHECKED_CAST") + (packages as MutableSet).add("com.google.android.wearable.app.cn") + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + + private fun spoofCscValue(lpparam: LoadPackageParam) { + try { + XposedHelpers.findAndHookMethod( + "com.samsung.android.app.twatchmanager.util.PlatformNetworkUtils", + lpparam.classLoader, + "getCSC", + object : XC_MethodReplacement() { + override fun replaceHookedMethod(param: MethodHookParam): Any { + return "TGY" + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } +} 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 6675dac..2af4c7c 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 @@ -13,8 +13,11 @@ 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.SelectItem import io.github.soclear.oneuix.ui.component.SwitchItem +private const val WATCH_PAIRING_MODE_CN = 1 + @Composable fun DetailPaneOther( uiState: Preference.Other, @@ -125,6 +128,34 @@ fun DetailPaneOther( checked = uiState.hideAppsSearchBar, onCheckedChange = { onEvent(OtherEvent.HideAppsSearchBar(it)) } ) + SelectItem( + icon = ImageVector.vectorResource(id = R.drawable.watch_pairing), + title = stringResource(id = R.string.watchPairing_connectionMode_title), + summary = stringResource(id = R.string.watchPairing_connectionMode_summary), + entries = listOf( + stringResource(id = R.string.watchPairing_mode_none), + stringResource(id = R.string.watchPairing_mode_wearos_cn), + stringResource(id = R.string.watchPairing_mode_wearos_global) + ), + selectedIndex = uiState.watchPairingConnectionMode, + onSelectedIndexChange = { onEvent(OtherEvent.WatchPairingConnectionMode(it)) } + ) + SwitchItem( + icon = ImageVector.vectorResource(id = R.drawable.lock_open), + title = stringResource(id = R.string.bypassWatchPairingRegionCheck_title), + summary = stringResource(id = R.string.bypassWatchPairingRegionCheck_summary), + checked = uiState.bypassWatchPairingRegionCheck, + onCheckedChange = { onEvent(OtherEvent.BypassWatchPairingRegionCheck(it)) } + ) + if (uiState.watchPairingConnectionMode == WATCH_PAIRING_MODE_CN) { + SwitchItem( + icon = ImageVector.vectorResource(id = R.drawable.google_play), + title = stringResource(id = R.string.supplementChinaWearOsGms_title), + summary = stringResource(id = R.string.supplementChinaWearOsGms_summary), + checked = uiState.supplementChinaWearOsGms, + onCheckedChange = { onEvent(OtherEvent.SupplementChinaWearOsGms(it)) } + ) + } } } @@ -174,6 +205,15 @@ sealed interface OtherEvent { @JvmInline value class HideAppsSearchBar(val value: Boolean) : OtherEvent + + @JvmInline + value class BypassWatchPairingRegionCheck(val value: Boolean) : OtherEvent + + @JvmInline + value class WatchPairingConnectionMode(val value: Int) : OtherEvent + + @JvmInline + value class SupplementChinaWearOsGms(val value: Boolean) : OtherEvent } fun SettingViewModel.onOtherEvent(event: OtherEvent) { @@ -268,6 +308,29 @@ fun SettingViewModel.onOtherEvent(event: OtherEvent) { hideAppsSearchBar = event.value ) ) + + is OtherEvent.BypassWatchPairingRegionCheck -> preference.copy( + other = preference.other.copy( + bypassWatchPairingRegionCheck = event.value + ) + ) + + is OtherEvent.WatchPairingConnectionMode -> preference.copy( + other = preference.other.copy( + watchPairingConnectionMode = event.value, + supplementChinaWearOsGms = if (event.value == WATCH_PAIRING_MODE_CN) { + preference.other.supplementChinaWearOsGms + } else { + false + } + ) + ) + + is OtherEvent.SupplementChinaWearOsGms -> preference.copy( + other = preference.other.copy( + supplementChinaWearOsGms = event.value + ) + ) } } } diff --git a/app/src/main/java/io/github/soclear/oneuix/ui/component/SelectItem.kt b/app/src/main/java/io/github/soclear/oneuix/ui/component/SelectItem.kt new file mode 100644 index 0000000..4e3f4eb --- /dev/null +++ b/app/src/main/java/io/github/soclear/oneuix/ui/component/SelectItem.kt @@ -0,0 +1,61 @@ +package io.github.soclear.oneuix.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun SelectItem( + title: String, + modifier: Modifier = Modifier, + summary: String? = null, + icon: ImageVector? = null, + entries: List, + selectedIndex: Int, + onSelectedIndexChange: (Int) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + Box(modifier = modifier) { + ListItem( + headlineContent = { Text(title) }, + supportingContent = { + Text(entries.getOrElse(selectedIndex) { summary ?: "" }) + }, + leadingContent = icon?.let { { Icon(it, title) } }, + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = true } + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.widthIn(min = 200.dp, max = 320.dp) + ) { + entries.forEachIndexed { index, label -> + DropdownMenuItem( + text = { Text(label) }, + onClick = { + onSelectedIndexChange(index) + expanded = false + } + ) + } + } + } +} diff --git a/app/src/main/res/drawable/google_play.xml b/app/src/main/res/drawable/google_play.xml new file mode 100644 index 0000000..74abe38 --- /dev/null +++ b/app/src/main/res/drawable/google_play.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/lock_open.xml b/app/src/main/res/drawable/lock_open.xml new file mode 100644 index 0000000..b663f2d --- /dev/null +++ b/app/src/main/res/drawable/lock_open.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/watch_pairing.xml b/app/src/main/res/drawable/watch_pairing.xml new file mode 100644 index 0000000..adc96f8 --- /dev/null +++ b/app/src/main/res/drawable/watch_pairing.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3fa48b6..65aa97c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -95,4 +95,13 @@ Masquer la barre d\'état sur l\'écran de verrouillage Redémarrer l\'UI système (root) Masquer la barre de recherche sur l\'écran des applications + Mode de connexion de la montre + Choisir la façon dont Galaxy Wearable gère le mode régional WearOS + Aucune modification (par défaut) + WearOS CN (GMS Chine) + WearOS Global (GMS global) + Contourner les vérifications régionales de l\'association + Contourner les vérifications de région, CSC et téléphone non pris en charge + Ajouter le GMS WearOS Chine + Si WearOS CN ne l\'ajoute pas automatiquement, ajouter com.google.android.wearable.app.cn à la liste de téléchargement diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f3d251a..f650162 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -95,4 +95,13 @@ Скрыть строку состояния на экране блокировки Перезапуск System UI (Root) Скрыть строку поиска на экране приложений + Режим подключения часов + Выберите, как Galaxy Wearable обрабатывает региональный режим WearOS + Без изменений (по умолчанию) + WearOS CN (китайский GMS) + WearOS Global (глобальный GMS) + Обход региональных проверок сопряжения часов + Обход проверок региона, CSC и неподдерживаемого телефона + Добавить China WearOS GMS + Если WearOS CN не добавляет его автоматически, добавить com.google.android.wearable.app.cn в список загрузки diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 06e2c6a..5dccf1f 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -95,4 +95,13 @@ 隐藏锁屏界面的状态栏 重启系统 UI (root) 隐藏应用屏幕搜索栏 + 手表连接模式 + 选择 Galaxy Wearable 处理 WearOS 区域模式的方式 + 不修改(默认) + WearOS CN(国行 GMS) + WearOS Global(外版 GMS) + 绕过手表配对区域检查 + 绕过区域、CSC 和不支持手机的配对检查 + 补充国行 WearOS GMS + 当 WearOS CN 未自动加入时,将 com.google.android.wearable.app.cn 加入下载列表 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index dc2fb8e..2ee4983 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -17,6 +17,7 @@ com.samsung.android.shealthmonitor com.samsung.android.themecenter com.samsung.android.video + com.samsung.android.app.watchmanager com.sec.android.app.camera com.sec.android.app.launcher com.sec.android.app.samsungapps diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e4f3f11..819ab1f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -95,4 +95,13 @@ Hide status bar on lock screen Reboot System UI (root) Hide search bar on app screen + Watch connection mode + Choose how Galaxy Wearable handles WearOS region mode + No modification (Default) + WearOS CN (China GMS) + WearOS Global (Global GMS) + Bypass watch pairing region checks + Bypass region, CSC, and unsupported-phone pairing checks + Supplement China WearOS GMS + If WearOS CN does not add it automatically, add com.google.android.wearable.app.cn to the download list From 12cc12e342990bb43053e1ac11cb83bceb934f70 Mon Sep 17 00:00:00 2001 From: Mzdyl <25149539+Mzdyl@users.noreply.github.com> Date: Tue, 12 May 2026 17:25:20 +0800 Subject: [PATCH 2/2] fix: avoid mutating immutable watch package set --- .../java/io/github/soclear/oneuix/hook/WatchPairing.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/WatchPairing.kt b/app/src/main/java/io/github/soclear/oneuix/hook/WatchPairing.kt index be881a5..174f155 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/WatchPairing.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/WatchPairing.kt @@ -134,9 +134,10 @@ object WatchPairing { "makePackageListToDownload", object : XC_MethodHook() { override fun afterHookedMethod(param: MethodHookParam) { - val packages = param.result as? MutableSet<*> ?: return - @Suppress("UNCHECKED_CAST") - (packages as MutableSet).add("com.google.android.wearable.app.cn") + val packages = param.result as? Set<*> ?: return + val newPackages = packages.toMutableSet() + newPackages.add("com.google.android.wearable.app.cn") + param.result = newPackages } } )