diff --git a/README.md b/README.md index ae7fcb4..82b98d8 100644 --- a/README.md +++ b/README.md @@ -465,9 +465,7 @@ These come from user reports. Each one links to its GitHub issue, where progress :construction: fix in progress · :hourglass_flowing_sand: fix released, waiting for user confirmation - :construction: **Samsung Health+ is very slow to get new data, and some of it can go missing** ([#9](https://github.com/LibreStatic/LibreWrist/issues/9)). Samsung Health+ has no direct link to the watch: the stock Samsung Health uploads the watch data to the Samsung cloud, and Samsung Health+ downloads it from there. That round trip can take more than a day. A sync can also stop halfway, or clash with data Samsung Health+ already has, and then part of the data is rejected. The problem is confirmed and a more reliable sync is in progress. Until then, the stock Samsung Health app has the up-to-date data. Samsung Health Monitor (ECG, blood pressure, IHRN, sleep apnea) is not affected, because it syncs straight from the watch. -- :hourglass_flowing_sand: **The watch rings for the Clock app's "upcoming alarm" reminder, not only for the alarm itself** ([#10](https://github.com/LibreStatic/LibreWrist/issues/10)). Some Clock apps (for example Xiaomi's) show a notification about an hour before an alarm. The phone bridge took that notification for a ringing alarm, so the watch rang then, and again each time the reminder came back. The bug is confirmed and the fix shipped in 0.9.3: update the patcher, then build again and use **Install on this phone** so the new phone bridge is installed. An affected user still needs to report that it works. Until then, turning off the upcoming alarm reminder in the Clock app avoids it. - :construction: **Several smaller Samsung Health Monitor problems on the watch** ([#13](https://github.com/LibreStatic/LibreWrist/issues/13)). A daily Health Monitor notification on the watch that cannot be dismissed until it goes away by itself, ECG readings that often fail, a heart rate that jumps a lot during an ECG, and sleep apnea tracking that reports not enough data every morning. The report is confirmed. ECG readings failed because a single brief loss of finger contact cancelled the reading; 0.10.0 ignores such short blips and also logs the beat timing during an ECG, so the heart-rate jumps can be traced from a device log. Sleep apnea tracking had no blood oxygen data because the overnight recording never started; 0.10.4 starts it by itself, and the user confirmed on 0.10.7 that a night now counts (use 0.10.6 or later: the watch build of 0.10.4 and 0.10.5 stopped with a "verify-anchors" error). Since 0.10.4 the recording also kept the watch awake all night, which drained the battery; 0.10.9 keeps it awake only for a moment per batch of sensor data, as Health Monitor does for its own sensors, and an affected user still needs to report that the battery is back to normal and nights still count. Every device log export includes the recording events of the last nights since 0.10.7. There is no button to end a night: Health Monitor completes it by itself after at least 4 hours of sleep and can take up to 90 minutes after you wake up. The heart-rate jumps and the notification still need a report from an affected user. -- :hourglass_flowing_sand: **Samsung Health+ can't sign in or finish setup on Samsung phones** ([#19](https://github.com/LibreStatic/LibreWrist/issues/19)). Samsung phones have Samsung services that only accept the original Samsung Health, and Samsung Health+ is a renamed copy. First, the Samsung Account app refused the sign-in (it answered that a Samsung account already exists); 0.10.3 fixed that by always signing in through a browser page, as on other phones. After that, setup stopped with "temporarily unavailable" (0x2700010): the app protects its data with a key from Samsung Knox, and Knox refuses this copy. The bug is confirmed. The fix shipped in 0.10.8: the app now uses the standard Android key store, as on other phones. Update the patcher, build Samsung Health+ again and reinstall it. An affected user still needs to report that it works on their Samsung phone. ## How it works diff --git a/patcher-app/app/build.gradle.kts b/patcher-app/app/build.gradle.kts index dc62fa2..26ea8ae 100644 --- a/patcher-app/app/build.gradle.kts +++ b/patcher-app/app/build.gradle.kts @@ -23,7 +23,7 @@ android { val catalogueMinApp = Regex("\"minAppVersion\":\\s*(\\d+)") .find(rootProject.file("../catalogue/catalogue.json").readText())?.groupValues?.get(1)?.toInt() ?: 1 versionCode = System.getenv("PATCHER_VERSION_CODE")?.toIntOrNull() ?: catalogueMinApp - versionName = "0.10.9" + versionName = "0.10.10" // Base URL of the raw `catalogue/` directory of the public repository (config.env CATALOGUE_URL, // or -PcatalogueUrl). Empty disables catalogue updates. Only https is accepted. val catalogueUrl = (providers.gradleProperty("catalogueUrl").orNull ?: System.getenv("CATALOGUE_URL") ?: "").trim() diff --git a/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallScreens.kt b/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallScreens.kt index df5126c..44ef07c 100644 --- a/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallScreens.kt +++ b/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallScreens.kt @@ -123,6 +123,8 @@ data class InstallUiState( val leftoversKept: List = emptyList(), /** Copies of an earlier prefix still on the phone, as last read by a refresh; the screen offers to remove them. */ val leftoversFound: List = emptyList(), + /** The phone install over Wireless debugging was blocked by the phone; the screen offers the platform installer. */ + val adbInstallRestricted: Boolean = false, ) { /** Everything is installed but some access is still open: the user may go on without it. */ val canSkipGrants: Boolean get() = !running && !done && items.isNotEmpty() && items.all { it.status == CheckStatus.Passed } @@ -163,6 +165,9 @@ sealed interface InstallEvent { /** The error dialog of a Wireless debugging run was acknowledged, or its success overlay ran out. */ data object AcknowledgeAdbOutcome : InstallEvent + + /** Install with the system's own confirmation, after the install over Wireless debugging was blocked (phone only). */ + data object InstallWithConfirmation : InstallEvent } @Composable @@ -338,6 +343,9 @@ private fun InstallStep( } } } + if (phone && state.adbInstallRestricted && !state.running) { + OutlinedAction(stringResource(R.string.action_install_with_confirmation), { onEvent(InstallEvent.InstallWithConfirmation) }, Icons.Rounded.Settings) + } if (state.needsUnknownSources) { OutlinedAction(stringResource(R.string.action_allow_installing), { onEvent(InstallEvent.OpenUnknownSources) }, Icons.Rounded.Settings) } diff --git a/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallSteps.kt b/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallSteps.kt index 15af058..c23b252 100644 --- a/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallSteps.kt +++ b/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallSteps.kt @@ -30,7 +30,20 @@ internal fun fail(state: MutableStateFlow, @StringRes res: Int, /** The message for a refused install; a copy with another signature is called out. */ @StringRes internal fun installFailureRes(code: String?): Int = - if (code == "INSTALL_FAILED_UPDATE_INCOMPATIBLE") R.string.install_error_signature else R.string.install_error_failed + when (code) { + "INSTALL_FAILED_UPDATE_INCOMPATIBLE" -> R.string.install_error_signature + INSTALL_FAILED_USER_RESTRICTED -> R.string.install_error_adb_restricted + else -> R.string.install_error_failed + } + +/** The package manager's code when the phone blocks installs over debugging (MIUI/HyperOS "Install via USB", OPPO/vivo prompt). */ +internal const val INSTALL_FAILED_USER_RESTRICTED = "INSTALL_FAILED_USER_RESTRICTED" + +/** + * True when `getprop persist.security.adbinstall` says installs over debugging are off (Xiaomi's + * "Install via USB" switch reads "0"). Other vendors print nothing, which is not a block. + */ +internal fun adbInstallBlocked(getpropOutput: String): Boolean = getpropOutput.trim() == "0" /** * True when a refused install means another key signed the installed copy: the package manager's diff --git a/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallViewModel.kt b/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallViewModel.kt index 867f0de..d883f0b 100644 --- a/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallViewModel.kt +++ b/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/InstallViewModel.kt @@ -125,6 +125,7 @@ class InstallPhoneViewModel(application: Application) : AndroidViewModel(applica InstallEvent.CancelReinstall -> cancelReinstall(_state) InstallEvent.ConfirmReinstall -> reinstall() InstallEvent.AcknowledgeAdbOutcome -> _state.update { it.copy(adbOutcome = null) } + InstallEvent.InstallWithConfirmation -> installWithConfirmation() } } @@ -154,6 +155,16 @@ class InstallPhoneViewModel(application: Application) : AndroidViewModel(applica viewModelScope.launch(Dispatchers.IO) { runner.run(_state) } } + /** + * The explicit way out when the phone blocks installs over debugging: the platform installer, + * with the system's confirmation. Only this tap does it; unattended mode never falls back by itself. + */ + private fun installWithConfirmation() { + if (_state.value.running) return + _state.update { it.copy(running = true, errorRes = null, errorDetail = null, adbInstallRestricted = false) } + viewModelScope.launch(Dispatchers.IO) { runner.run(_state) } + } + /** What unattended mode does once the phone's own ADB session is connected, before the grants. */ private enum class PendingInstall { Install, Reinstall } @@ -171,7 +182,7 @@ class InstallPhoneViewModel(application: Application) : AndroidViewModel(applica pending = null return } - _state.update { it.copy(running = true, confirmingReinstall = false, errorRes = null, errorDetail = null, adbOutcome = null) } + _state.update { it.copy(running = true, confirmingReinstall = false, errorRes = null, errorDetail = null, adbOutcome = null, adbInstallRestricted = false) } settingsOpened = false SelfAdbService.start(getApplication()) } @@ -265,11 +276,26 @@ class InstallPhoneViewModel(application: Application) : AndroidViewModel(applica } } + /** Xiaomi's "Install via USB" switch, read before any APK is sent. Unknown (shell error) counts as not blocked. */ + private suspend fun isBlockedByPhone(client: AdbClient): Boolean = try { + adbInstallBlocked(client.shell("getprop persist.security.adbinstall").stdout) + } catch (e: kotlinx.coroutines.CancellationException) { + throw e + } catch (e: Exception) { + Log.w(TAG, "install pre-check skipped", e) + false + } + /** * The install (or the replacement of a copy another key signed) through [client]. True when every * app is installed and the grants can follow; otherwise the runner set the error (Retry shows). */ private suspend fun installOverAdb(client: AdbClient, action: PendingInstall): Boolean { + if (isBlockedByPhone(client)) { + _state.update { it.copy(running = false, adbInstallRestricted = true) } + fail(_state, R.string.install_error_adb_restricted, "persist.security.adbinstall=0") + return false + } runner.installerOverride = AdbPhoneInstaller(client, sessionInstaller) try { when (action) { diff --git a/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/PhoneInstallRunner.kt b/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/PhoneInstallRunner.kt index 22db519..da1c50a 100644 --- a/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/PhoneInstallRunner.kt +++ b/patcher-app/app/src/main/kotlin/io/openshm/patcher/ui/screens/install/PhoneInstallRunner.kt @@ -74,7 +74,7 @@ internal class PhoneInstallRunner( locked = false state.update { it.copy( - running = true, errorRes = null, errorDetail = null, needsUnknownSources = false, conflict = null, confirmingReinstall = false, + running = true, errorRes = null, errorDetail = null, adbInstallRestricted = false, needsUnknownSources = false, conflict = null, confirmingReinstall = false, removingLeftovers = emptyList(), leftoversKept = emptyList(), ) } @@ -217,7 +217,10 @@ internal class PhoneInstallRunner( kind = installer.packageKind(item.packageName), detail = detail, ) - if (!offered) fail(state, installFailureRes(result.code), detail) + if (!offered) { + fail(state, installFailureRes(result.code), detail) + if (result.code == INSTALL_FAILED_USER_RESTRICTED) state.update { it.copy(adbInstallRestricted = true) } + } false } } diff --git a/patcher-app/app/src/main/res/values-ar/strings.xml b/patcher-app/app/src/main/res/values-ar/strings.xml index 832eeca..05da16f 100644 --- a/patcher-app/app/src/main/res/values-ar/strings.xml +++ b/patcher-app/app/src/main/res/values-ar/strings.xml @@ -329,9 +329,11 @@ الإصدار المبني غير موجود أو لا يطابق هذا الإعداد. ابنِ مرة أخرى ثم ثبّت. رُفض التثبيت. توجد نسخة مثبّتة موقّعة بمفتاح مختلف. لم يُزَل شيء. أزل تثبيت تلك النسخة بنفسك ثم حاول مرة أخرى. + يمنع هذا الهاتف التثبيت عبر التصحيح. على Xiaomi أو Redmi أو POCO فعّل خيارات المطور › التثبيت عبر USB. على OPPO أو realme أو vivo اقبل الرسالة التي يعرضها الهاتف أو أوقف مراقبة الأذونات. أو ثبّت بدلاً من ذلك بتأكيد من النظام. تعذّر تطبيق أحد إعدادات الوصول. فتح السماح بتثبيت التطبيقات + التثبيت مع التأكيد افحص مرة أخرى متابعة على أي حال افتح Samsung Health Monitor وأكمل إعداده diff --git a/patcher-app/app/src/main/res/values-de/strings.xml b/patcher-app/app/src/main/res/values-de/strings.xml index 7b03f63..6029743 100644 --- a/patcher-app/app/src/main/res/values-de/strings.xml +++ b/patcher-app/app/src/main/res/values-de/strings.xml @@ -301,9 +301,11 @@ Der Build fehlt oder passt nicht zu dieser Einrichtung. Erstelle ihn neu und installiere dann. Die Installation wurde abgelehnt. Es ist bereits eine Kopie installiert, die mit einem anderen Schlüssel signiert ist. Es wurde nichts entfernt. Deinstalliere diese Kopie selbst und versuche es dann erneut. + Dieses Telefon blockiert Installationen über Debugging. Aktiviere bei Xiaomi, Redmi oder POCO unter Entwickleroptionen › Über USB installieren. Bestätige bei OPPO, realme oder vivo die Abfrage des Telefons oder deaktiviere die Berechtigungsüberwachung. Oder installiere stattdessen mit der Bestätigung des Systems. Eine Zugriffseinstellung konnte nicht angewendet werden. Öffnen Installation von Apps erlauben + Mit Bestätigung installieren Erneut prüfen Trotzdem fortfahren Samsung Health Monitor öffnen und die Einrichtung abschließen diff --git a/patcher-app/app/src/main/res/values-es/strings.xml b/patcher-app/app/src/main/res/values-es/strings.xml index 7345cb7..f339a3b 100644 --- a/patcher-app/app/src/main/res/values-es/strings.xml +++ b/patcher-app/app/src/main/res/values-es/strings.xml @@ -301,9 +301,11 @@ Falta la compilación o no coincide con esta configuración. Compila de nuevo y luego instala. Se rechazó la instalación. Ya hay instalada una copia firmada con otra clave. No se quitó nada. Desinstala esa copia tú mismo y vuelve a intentarlo. + Este teléfono bloquea las instalaciones por depuración. En Xiaomi, Redmi o POCO, activa Opciones de desarrollador › Instalar vía USB. En OPPO, realme o vivo, acepta el aviso que muestra el teléfono o desactiva la supervisión de permisos. O instala con la confirmación del sistema. No se pudo aplicar un ajuste de acceso. Abrir Permitir instalar apps + Instalar con confirmación Comprobar de nuevo Continuar de todos modos Abre Samsung Health Monitor y termina su configuración diff --git a/patcher-app/app/src/main/res/values-fr/strings.xml b/patcher-app/app/src/main/res/values-fr/strings.xml index 94a9a8a..d1e28e3 100644 --- a/patcher-app/app/src/main/res/values-fr/strings.xml +++ b/patcher-app/app/src/main/res/values-fr/strings.xml @@ -308,9 +308,11 @@ La compilation est absente ou ne correspond pas à cette configuration. Compilez à nouveau, puis installez. L\'installation a été refusée. Une copie signée avec une autre clé est déjà installée. Rien n\'a été supprimé. Désinstallez vous-même cette copie, puis réessayez. + Ce téléphone bloque les installations par débogage. Sur Xiaomi, Redmi ou POCO, activez Options pour les développeurs › Installer via USB. Sur OPPO, realme ou vivo, acceptez la demande affichée par le téléphone ou désactivez la surveillance des autorisations. Ou installez avec la confirmation du système. Un paramètre d\'accès n\'a pas pu être appliqué. Ouvrir Autoriser l\'installation d\'apps + Installer avec confirmation Vérifier à nouveau Continuer quand même Ouvrez Samsung Health Monitor et terminez sa configuration diff --git a/patcher-app/app/src/main/res/values-hi/strings.xml b/patcher-app/app/src/main/res/values-hi/strings.xml index c779d80..8472526 100644 --- a/patcher-app/app/src/main/res/values-hi/strings.xml +++ b/patcher-app/app/src/main/res/values-hi/strings.xml @@ -301,9 +301,11 @@ बिल्ड मौजूद नहीं है या इस सेटअप से मेल नहीं खाता। दोबारा बिल्ड करें, फिर इंस्टॉल करें। इंस्टॉल अस्वीकार कर दिया गया। दूसरी कुंजी से साइन की गई एक कॉपी पहले से इंस्टॉल है। कुछ भी हटाया नहीं गया। उस कॉपी को खुद अनइंस्टॉल करें, फिर दोबारा कोशिश करें। + यह फ़ोन डीबगिंग के ज़रिए इंस्टॉल करने से रोकता है। Xiaomi, Redmi या POCO पर डेवलपर विकल्प › USB के ज़रिए इंस्टॉल करें चालू करें। OPPO, realme या vivo पर फ़ोन में दिखने वाला संकेत स्वीकार करें या अनुमति निगरानी बंद करें। या इसके बजाय सिस्टम की पुष्टि के साथ इंस्टॉल करें। एक पहुँच सेटिंग लागू नहीं हो सकी। खोलें ऐप इंस्टॉल करने की अनुमति दें + पुष्टि के साथ इंस्टॉल करें दोबारा जाँचें फिर भी जारी रखें Samsung Health Monitor खोलें और उसका सेटअप पूरा करें diff --git a/patcher-app/app/src/main/res/values-in/strings.xml b/patcher-app/app/src/main/res/values-in/strings.xml index a0440a1..c2e55a2 100644 --- a/patcher-app/app/src/main/res/values-in/strings.xml +++ b/patcher-app/app/src/main/res/values-in/strings.xml @@ -294,9 +294,11 @@ Build tidak ada atau tidak cocok dengan penyiapan ini. Bangun lagi, lalu pasang. Pemasangan ditolak. Salinan yang ditandatangani dengan kunci lain sudah terpasang. Tidak ada yang dihapus. Copot sendiri salinan itu, lalu coba lagi. + Ponsel ini memblokir instalasi lewat debugging. Di Xiaomi, Redmi, atau POCO, aktifkan Opsi pengembang › Instal via USB. Di OPPO, realme, atau vivo, setujui perintah yang ditampilkan ponsel, atau matikan Pemantauan izin. Atau instal dengan konfirmasi sistem. Satu setelan akses tidak dapat diterapkan. Buka Izinkan memasang aplikasi + Instal dengan konfirmasi Periksa lagi Tetap lanjutkan Buka Samsung Health Monitor dan selesaikan penyiapannya diff --git a/patcher-app/app/src/main/res/values-it/strings.xml b/patcher-app/app/src/main/res/values-it/strings.xml index afbb3c5..3681680 100644 --- a/patcher-app/app/src/main/res/values-it/strings.xml +++ b/patcher-app/app/src/main/res/values-it/strings.xml @@ -302,9 +302,11 @@ La build manca o non corrisponde a questa configurazione. Compila di nuovo, poi installa. L\'installazione è stata rifiutata. È già installata una copia firmata con una chiave diversa. Non è stato rimosso nulla. Disinstalla tu quella copia, poi riprova. + Questo telefono blocca le installazioni tramite debug. Su Xiaomi, Redmi o POCO attiva Opzioni sviluppatore › Installa via USB. Su OPPO, realme o vivo accetta la richiesta mostrata dal telefono oppure disattiva il monitoraggio delle autorizzazioni. Oppure installa con la conferma del sistema. Non è stato possibile applicare un\'impostazione di accesso. Apri Consenti l\'installazione di app + Installa con conferma Controlla di nuovo Continua comunque Apri Samsung Health Monitor e completa la configurazione diff --git a/patcher-app/app/src/main/res/values-ja/strings.xml b/patcher-app/app/src/main/res/values-ja/strings.xml index 7d07890..1859988 100644 --- a/patcher-app/app/src/main/res/values-ja/strings.xml +++ b/patcher-app/app/src/main/res/values-ja/strings.xml @@ -294,9 +294,11 @@ ビルドが見つからないか、このセットアップと一致しません。もう一度ビルドしてからインストールしてください。 インストールが拒否されました。 別のキーで署名されたコピーがすでにインストールされています。何も削除していません。そのコピーをご自身でアンインストールしてから、もう一度お試しください。 + このスマートフォンはデバッグ経由のインストールをブロックしています。Xiaomi、Redmi、POCO では 開発者向けオプション › USB経由でインストール をオンにしてください。OPPO、realme、vivo では端末に表示される確認を許可するか、権限モニタリングをオフにしてください。または、システムの確認を使ってインストールします。 アクセス設定を適用できませんでした。 開く アプリのインストールを許可 + 確認してインストール 再確認 このまま続ける Samsung Health Monitor を開いて、セットアップを完了する diff --git a/patcher-app/app/src/main/res/values-ko/strings.xml b/patcher-app/app/src/main/res/values-ko/strings.xml index 6249f9b..5a05108 100644 --- a/patcher-app/app/src/main/res/values-ko/strings.xml +++ b/patcher-app/app/src/main/res/values-ko/strings.xml @@ -294,9 +294,11 @@ 빌드가 없거나 이 설정과 일치하지 않습니다. 다시 빌드한 다음 설치하세요. 설치가 거부되었습니다. 다른 키로 서명된 사본이 이미 설치되어 있습니다. 아무것도 제거하지 않았습니다. 해당 사본을 직접 제거한 다음 다시 시도하세요. + 이 휴대전화는 디버깅을 통한 설치를 차단합니다. Xiaomi, Redmi, POCO에서는 개발자 옵션 › USB를 통해 설치를 켜세요. OPPO, realme, vivo에서는 휴대전화에 표시되는 안내를 허용하거나 권한 모니터링을 끄세요. 또는 시스템 확인 창을 통해 설치하세요. 접근 설정을 적용하지 못했습니다. 열기 앱 설치 허용 + 확인 후 설치 다시 확인 그래도 계속 Samsung Health Monitor를 열고 설정을 완료하세요 diff --git a/patcher-app/app/src/main/res/values-nl/strings.xml b/patcher-app/app/src/main/res/values-nl/strings.xml index 625752e..6a49d2f 100644 --- a/patcher-app/app/src/main/res/values-nl/strings.xml +++ b/patcher-app/app/src/main/res/values-nl/strings.xml @@ -301,9 +301,11 @@ De build ontbreekt of past niet bij deze configuratie. Bouw opnieuw en installeer dan. De installatie is geweigerd. Er is al een kopie geïnstalleerd die met een andere sleutel is ondertekend. Er is niets verwijderd. Verwijder die kopie zelf en probeer het daarna opnieuw. + Deze telefoon blokkeert installaties via foutopsporing. Zet op Xiaomi, Redmi of POCO Ontwikkelaarsopties › Installeren via USB aan. Accepteer op OPPO, realme of vivo de melding van de telefoon, of zet Machtigingsbewaking uit. Of installeer met de bevestiging van het systeem. Een toegangsinstelling kon niet worden toegepast. Openen Apps installeren toestaan + Installeren met bevestiging Opnieuw controleren Toch doorgaan Open Samsung Health Monitor en rond de configuratie ervan af diff --git a/patcher-app/app/src/main/res/values-pl/strings.xml b/patcher-app/app/src/main/res/values-pl/strings.xml index b4ca6d0..e984e95 100644 --- a/patcher-app/app/src/main/res/values-pl/strings.xml +++ b/patcher-app/app/src/main/res/values-pl/strings.xml @@ -315,9 +315,11 @@ Brak kompilacji lub nie pasuje do tej konfiguracji. Zbuduj ponownie, a potem zainstaluj. Instalacja została odrzucona. Jest już zainstalowana kopia podpisana innym kluczem. Nic nie zostało usunięte. Odinstaluj tę kopię samodzielnie, a potem spróbuj ponownie. + Ten telefon blokuje instalację przez debugowanie. Na Xiaomi, Redmi lub POCO włącz Opcje programisty › Instaluj przez USB. Na OPPO, realme lub vivo zaakceptuj komunikat wyświetlony na telefonie albo wyłącz monitorowanie uprawnień. Możesz też zainstalować z potwierdzeniem systemu. Nie udało się zastosować ustawienia dostępu. Otwórz Zezwól na instalowanie aplikacji + Zainstaluj z potwierdzeniem Sprawdź ponownie Kontynuuj mimo to Otwórz Samsung Health Monitor i dokończ jego konfigurację diff --git a/patcher-app/app/src/main/res/values-pt/strings.xml b/patcher-app/app/src/main/res/values-pt/strings.xml index 5445fee..cd14ca1 100644 --- a/patcher-app/app/src/main/res/values-pt/strings.xml +++ b/patcher-app/app/src/main/res/values-pt/strings.xml @@ -302,9 +302,11 @@ A compilação não existe ou não corresponde a esta configuração. Compile de novo e depois instale. A instalação foi recusada. Já há uma cópia instalada assinada com outra chave. Nada foi removido. Desinstale essa cópia você mesmo e tente novamente. + Este telefone bloqueia instalações por depuração. Em Xiaomi, Redmi ou POCO, ative Opções do programador › Instalar via USB. Em OPPO, realme ou vivo, aceite o aviso que o telefone mostra ou desative a monitorização de permissões. Ou instale com a confirmação do sistema. Não foi possível aplicar uma configuração de acesso. Abrir Permitir instalar apps + Instalar com confirmação Verificar novamente Continuar mesmo assim Abra o Samsung Health Monitor e conclua a configuração dele diff --git a/patcher-app/app/src/main/res/values-ru/strings.xml b/patcher-app/app/src/main/res/values-ru/strings.xml index f512f26..211eb77 100644 --- a/patcher-app/app/src/main/res/values-ru/strings.xml +++ b/patcher-app/app/src/main/res/values-ru/strings.xml @@ -315,9 +315,11 @@ Сборка отсутствует или не соответствует этой настройке. Соберите заново, затем установите. Установка отклонена. Уже установлена копия, подписанная другим ключом. Ничего не удалено. Удалите эту копию сами и повторите попытку. + Этот телефон блокирует установку через отладку. На Xiaomi, Redmi и POCO включите «Для разработчиков» › «Установка через USB». На OPPO, realme и vivo подтвердите запрос на телефоне или отключите контроль разрешений. Либо установите с подтверждением системы. Не удалось применить настройку доступа. Открыть Разрешить установку приложений + Установить с подтверждением Проверить снова Всё равно продолжить Откройте Samsung Health Monitor и завершите его настройку diff --git a/patcher-app/app/src/main/res/values-tr/strings.xml b/patcher-app/app/src/main/res/values-tr/strings.xml index 52277e3..3027e4a 100644 --- a/patcher-app/app/src/main/res/values-tr/strings.xml +++ b/patcher-app/app/src/main/res/values-tr/strings.xml @@ -300,9 +300,11 @@ Derleme yok veya bu kurulumla uyuşmuyor. Yeniden derleyin, sonra kurun. Kurulum reddedildi. Farklı bir anahtarla imzalanmış bir kopya zaten kurulu. Hiçbir şey kaldırılmadı. O kopyayı kendiniz kaldırın, sonra tekrar deneyin. + Bu telefon hata ayıklama üzerinden kurulumu engelliyor. Xiaomi, Redmi veya POCO\'da Geliştirici seçenekleri › USB ile yükle seçeneğini açın. OPPO, realme veya vivo\'da telefonun gösterdiği istemi kabul edin ya da izin izlemeyi kapatın. Ya da sistemin onayıyla kurun. Bir erişim ayarı uygulanamadı. Aç Uygulama kurulumuna izin ver + Onayla kur Tekrar denetle Yine de devam et Samsung Health Monitor\'ü açıp kurulumunu bitirin diff --git a/patcher-app/app/src/main/res/values-uk/strings.xml b/patcher-app/app/src/main/res/values-uk/strings.xml index f21f86a..c910180 100644 --- a/patcher-app/app/src/main/res/values-uk/strings.xml +++ b/patcher-app/app/src/main/res/values-uk/strings.xml @@ -315,9 +315,11 @@ Збірка відсутня або не відповідає цьому налаштуванню. Зберіть знову, потім встановіть. Встановлення відхилено. Уже встановлено копію, підписану іншим ключем. Нічого не видалено. Видаліть цю копію самі й спробуйте ще раз. + Цей телефон блокує встановлення через налагодження. На Xiaomi, Redmi або POCO увімкніть «Для розробників» › «Встановлення через USB». На OPPO, realme або vivo підтвердьте запит на телефоні або вимкніть контроль дозволів. Або встановіть із підтвердженням системи. Не вдалося застосувати налаштування доступу. Відкрити Дозволити встановлення застосунків + Встановити з підтвердженням Перевірити знову Усе одно продовжити Відкрийте Samsung Health Monitor і завершіть його налаштування diff --git a/patcher-app/app/src/main/res/values-zh-rCN/strings.xml b/patcher-app/app/src/main/res/values-zh-rCN/strings.xml index 123b78d..8c7cfff 100644 --- a/patcher-app/app/src/main/res/values-zh-rCN/strings.xml +++ b/patcher-app/app/src/main/res/values-zh-rCN/strings.xml @@ -294,9 +294,11 @@ 构建缺失,或与此次设置不匹配。请重新构建后再安装。 安装被拒绝。 已安装使用不同密钥签名的副本。未移除任何内容。请自行卸载该副本,然后重试。 + 此手机阻止通过调试安装。在小米、红米或 POCO 上,请开启 开发者选项 › 通过 USB 安装。在 OPPO、realme 或 vivo 上,请允许手机显示的提示,或关闭权限监控。也可以改用系统确认方式安装。 无法应用某项访问设置。 打开 允许安装应用 + 确认后安装 重新检查 仍然继续 打开 Samsung Health Monitor 并完成其设置 diff --git a/patcher-app/app/src/main/res/values-zh-rTW/strings.xml b/patcher-app/app/src/main/res/values-zh-rTW/strings.xml index bd512dd..6903c96 100644 --- a/patcher-app/app/src/main/res/values-zh-rTW/strings.xml +++ b/patcher-app/app/src/main/res/values-zh-rTW/strings.xml @@ -294,9 +294,11 @@ 建置遺失,或與此次設定不符。請重新建置後再安裝。 安裝遭到拒絕。 已安裝使用不同金鑰簽署的副本。未移除任何內容。請自行解除安裝該副本,然後再試一次。 + 此手機會阻擋透過偵錯安裝。在小米、紅米或 POCO 上,請開啟 開發人員選項 › 透過 USB 安裝。在 OPPO、realme 或 vivo 上,請允許手機顯示的提示,或關閉權限監控。也可以改用系統確認方式安裝。 無法套用某項存取設定。 開啟 允許安裝應用程式 + 確認後安裝 重新檢查 仍要繼續 開啟 Samsung Health Monitor 並完成其設定 diff --git a/patcher-app/app/src/main/res/values/strings.xml b/patcher-app/app/src/main/res/values/strings.xml index dbd3008..8379ece 100644 --- a/patcher-app/app/src/main/res/values/strings.xml +++ b/patcher-app/app/src/main/res/values/strings.xml @@ -408,9 +408,11 @@ The build is missing or does not match this setup. Build again, then install. The install was refused. A copy signed with a different key is already installed. Nothing was removed. Uninstall that copy yourself, then try again. + This phone blocks installs over debugging. On Xiaomi, Redmi or POCO, turn on Developer options › Install via USB. On OPPO, realme or vivo, accept the prompt the phone shows, or turn off Permission monitoring. Or install with the system\'s confirmation instead. An access setting could not be applied. Open Allow installing apps + Install with confirmation Check again Continue anyway Open Samsung Health Monitor and finish its setup diff --git a/patcher-app/app/src/test/kotlin/io/openshm/patcher/AdbInstallRestrictedTest.kt b/patcher-app/app/src/test/kotlin/io/openshm/patcher/AdbInstallRestrictedTest.kt new file mode 100644 index 0000000..298ac7c --- /dev/null +++ b/patcher-app/app/src/test/kotlin/io/openshm/patcher/AdbInstallRestrictedTest.kt @@ -0,0 +1,141 @@ +package io.openshm.patcher + +import io.openshm.patcher.adb.InstallResult +import io.openshm.patcher.build.BuiltBuild +import io.openshm.patcher.build.BuiltRecord +import io.openshm.patcher.device.PackageKind +import io.openshm.patcher.device.PhoneGrants +import io.openshm.patcher.device.PhoneInstaller +import io.openshm.patcher.device.PhoneProbe +import io.openshm.patcher.device.UninstallResult +import io.openshm.patcher.ui.components.CheckStatus +import io.openshm.patcher.ui.screens.install.InstallItem +import io.openshm.patcher.ui.screens.install.InstallUiState +import io.openshm.patcher.ui.screens.install.PhoneInstallRunner +import io.openshm.patcher.ui.screens.install.adbInstallBlocked +import io.openshm.patcher.ui.screens.install.installFailureRes +import java.io.File +import java.nio.file.Files +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +private const val PREFIX = "io.openshm.u123456" +private const val RESTRICTED = "INSTALL_FAILED_USER_RESTRICTED" + +private class RestrictedNoProbe : PhoneProbe { + override fun permissionGranted(pkg: String, permission: String) = false + override fun appOpAllowed(pkg: String, op: String): Boolean? = null + override fun listenerEnabled(pkg: String, component: String) = false +} + +private class ScriptedPhone(var result: InstallResult) : PhoneInstaller { + val calls = mutableListOf() + override fun canInstallPackages() = true + override fun isUpToDate(apk: File, packageName: String) = false + override fun packageKind(packageName: String) = PackageKind.System + override suspend fun uninstall(packageName: String) = UninstallResult.Success + override fun startBridge(packageName: String, receiver: String, action: String) {} + override suspend fun install(apk: File, packageName: String, onProgress: (Float) -> Unit): InstallResult { + calls += packageName + return result + } +} + +class AdbInstallRestrictedTest { + private lateinit var dir: File + private val restricted = InstallResult.Failure(RESTRICTED, "Install canceled by user", InstallResult.Stage.COMMIT) + + @Before fun setUp() { + dir = Files.createTempDirectory("adb-restricted").toFile() + TestCatalogue.install() + } + + @After fun tearDown() { + dir.deleteRecursively() + } + + private fun built() = BuiltBuild( + "k", PREFIX, 1, 0, + listOf("shm-phone").map { id -> File(dir, "$id.apk").apply { writeBytes(ByteArray(8) { 1 }) }.let { BuiltRecord(id, it.path, it.length(), "00") } }, + ) + + private fun state() = MutableStateFlow( + InstallUiState(listOf(InstallItem("shm-phone", "Health Monitor", "$PREFIX.shm", CheckStatus.Pending))), + ) + + private fun runner(installer: PhoneInstaller) = PhoneInstallRunner(installer, PhoneGrants(RestrictedNoProbe()), { built() }, { PREFIX }, {}) + + @Test + fun theRestrictedCodeHasItsOwnMessage() { + assertEquals(R.string.install_error_adb_restricted, installFailureRes(RESTRICTED)) + assertEquals(R.string.install_error_signature, installFailureRes("INSTALL_FAILED_UPDATE_INCOMPATIBLE")) + assertEquals(R.string.install_error_failed, installFailureRes("INSTALL_FAILED_VERSION_DOWNGRADE")) + assertEquals(R.string.install_error_failed, installFailureRes(null)) + } + + @Test + fun aRestrictedInstallSetsTheFlagAndTheMessage() = runBlocking { + val s = state() + runner(ScriptedPhone(restricted)).run(s) + assertEquals(R.string.install_error_adb_restricted, s.value.errorRes) + assertTrue(s.value.adbInstallRestricted) + assertFalse(s.value.running) + } + + @Test + fun aGenericFailureLeavesTheFlagOff() = runBlocking { + val s = state() + runner(ScriptedPhone(InstallResult.Failure("INSTALL_FAILED_INVALID_APK", "bad", InstallResult.Stage.COMMIT))).run(s) + assertEquals(R.string.install_error_failed, s.value.errorRes) + assertFalse(s.value.adbInstallRestricted) + } + + @Test + fun aNewRunClearsTheFlag() = runBlocking { + val s = state() + val phone = ScriptedPhone(restricted) + val r = runner(phone) + r.run(s) + assertTrue(s.value.adbInstallRestricted) + phone.result = InstallResult.Success(1) + r.run(s) + assertFalse(s.value.adbInstallRestricted) + assertNull(s.value.errorRes) + assertEquals(CheckStatus.Passed, s.value.items.single().status) + } + + @Test + fun withoutTheOverrideTheRunUsesThePlatformInstaller() = runBlocking { + val platform = ScriptedPhone(InstallResult.Success(1)) + val viaAdb = ScriptedPhone(restricted) + val r = runner(platform) + r.installerOverride = viaAdb + val s = state() + r.run(s) + assertEquals(listOf("$PREFIX.shm"), viaAdb.calls) + assertTrue(platform.calls.isEmpty()) + assertTrue(s.value.adbInstallRestricted) + r.installerOverride = null + r.run(s) + assertEquals(listOf("$PREFIX.shm"), platform.calls) + assertFalse(s.value.adbInstallRestricted) + } + + @Test + fun onlyAZeroFlagBlocks() { + assertTrue(adbInstallBlocked("0\n")) + assertTrue(adbInstallBlocked(" 0 ")) + assertFalse(adbInstallBlocked("1")) + assertFalse(adbInstallBlocked("")) + assertFalse(adbInstallBlocked("\n")) + assertFalse(adbInstallBlocked("00")) + assertFalse(adbInstallBlocked("getprop: not found")) + } +}