Skip to content
Merged
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: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion patcher-app/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ data class InstallUiState(
val leftoversKept: List<String> = 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<String> = 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 }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,20 @@ internal fun fail(state: MutableStateFlow<InstallUiState>, @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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -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 }

Expand All @@ -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())
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
}
Expand Down Expand Up @@ -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
}
}
Expand Down
2 changes: 2 additions & 0 deletions patcher-app/app/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,11 @@
<string name="install_error_build_missing">الإصدار المبني غير موجود أو لا يطابق هذا الإعداد. ابنِ مرة أخرى ثم ثبّت.</string>
<string name="install_error_failed">رُفض التثبيت.</string>
<string name="install_error_signature">توجد نسخة مثبّتة موقّعة بمفتاح مختلف. لم يُزَل شيء. أزل تثبيت تلك النسخة بنفسك ثم حاول مرة أخرى.</string>
<string name="install_error_adb_restricted">يمنع هذا الهاتف التثبيت عبر التصحيح. على Xiaomi أو Redmi أو POCO فعّل خيارات المطور › التثبيت عبر USB. على OPPO أو realme أو vivo اقبل الرسالة التي يعرضها الهاتف أو أوقف مراقبة الأذونات. أو ثبّت بدلاً من ذلك بتأكيد من النظام.</string>
<string name="install_error_grant">تعذّر تطبيق أحد إعدادات الوصول.</string>
<string name="action_open">فتح</string>
<string name="action_allow_installing">السماح بتثبيت التطبيقات</string>
<string name="action_install_with_confirmation">التثبيت مع التأكيد</string>
<string name="install_check_again">افحص مرة أخرى</string>
<string name="install_continue_anyway">متابعة على أي حال</string>
<string name="install_onboarding">افتح Samsung Health Monitor وأكمل إعداده</string>
Expand Down
2 changes: 2 additions & 0 deletions patcher-app/app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,11 @@
<string name="install_error_build_missing">Der Build fehlt oder passt nicht zu dieser Einrichtung. Erstelle ihn neu und installiere dann.</string>
<string name="install_error_failed">Die Installation wurde abgelehnt.</string>
<string name="install_error_signature">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.</string>
<string name="install_error_adb_restricted">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.</string>
<string name="install_error_grant">Eine Zugriffseinstellung konnte nicht angewendet werden.</string>
<string name="action_open">Öffnen</string>
<string name="action_allow_installing">Installation von Apps erlauben</string>
<string name="action_install_with_confirmation">Mit Bestätigung installieren</string>
<string name="install_check_again">Erneut prüfen</string>
<string name="install_continue_anyway">Trotzdem fortfahren</string>
<string name="install_onboarding">Samsung Health Monitor öffnen und die Einrichtung abschließen</string>
Expand Down
2 changes: 2 additions & 0 deletions patcher-app/app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,11 @@
<string name="install_error_build_missing">Falta la compilación o no coincide con esta configuración. Compila de nuevo y luego instala.</string>
<string name="install_error_failed">Se rechazó la instalación.</string>
<string name="install_error_signature">Ya hay instalada una copia firmada con otra clave. No se quitó nada. Desinstala esa copia tú mismo y vuelve a intentarlo.</string>
<string name="install_error_adb_restricted">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.</string>
<string name="install_error_grant">No se pudo aplicar un ajuste de acceso.</string>
<string name="action_open">Abrir</string>
<string name="action_allow_installing">Permitir instalar apps</string>
<string name="action_install_with_confirmation">Instalar con confirmación</string>
<string name="install_check_again">Comprobar de nuevo</string>
<string name="install_continue_anyway">Continuar de todos modos</string>
<string name="install_onboarding">Abre Samsung Health Monitor y termina su configuración</string>
Expand Down
2 changes: 2 additions & 0 deletions patcher-app/app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,11 @@
<string name="install_error_build_missing">La compilation est absente ou ne correspond pas à cette configuration. Compilez à nouveau, puis installez.</string>
<string name="install_error_failed">L\'installation a été refusée.</string>
<string name="install_error_signature">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.</string>
<string name="install_error_adb_restricted">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.</string>
<string name="install_error_grant">Un paramètre d\'accès n\'a pas pu être appliqué.</string>
<string name="action_open">Ouvrir</string>
<string name="action_allow_installing">Autoriser l\'installation d\'apps</string>
<string name="action_install_with_confirmation">Installer avec confirmation</string>
<string name="install_check_again">Vérifier à nouveau</string>
<string name="install_continue_anyway">Continuer quand même</string>
<string name="install_onboarding">Ouvrez Samsung Health Monitor et terminez sa configuration</string>
Expand Down
2 changes: 2 additions & 0 deletions patcher-app/app/src/main/res/values-hi/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,11 @@
<string name="install_error_build_missing">बिल्ड मौजूद नहीं है या इस सेटअप से मेल नहीं खाता। दोबारा बिल्ड करें, फिर इंस्टॉल करें।</string>
<string name="install_error_failed">इंस्टॉल अस्वीकार कर दिया गया।</string>
<string name="install_error_signature">दूसरी कुंजी से साइन की गई एक कॉपी पहले से इंस्टॉल है। कुछ भी हटाया नहीं गया। उस कॉपी को खुद अनइंस्टॉल करें, फिर दोबारा कोशिश करें।</string>
<string name="install_error_adb_restricted">यह फ़ोन डीबगिंग के ज़रिए इंस्टॉल करने से रोकता है। Xiaomi, Redmi या POCO पर डेवलपर विकल्प › USB के ज़रिए इंस्टॉल करें चालू करें। OPPO, realme या vivo पर फ़ोन में दिखने वाला संकेत स्वीकार करें या अनुमति निगरानी बंद करें। या इसके बजाय सिस्टम की पुष्टि के साथ इंस्टॉल करें।</string>
<string name="install_error_grant">एक पहुँच सेटिंग लागू नहीं हो सकी।</string>
<string name="action_open">खोलें</string>
<string name="action_allow_installing">ऐप इंस्टॉल करने की अनुमति दें</string>
<string name="action_install_with_confirmation">पुष्टि के साथ इंस्टॉल करें</string>
<string name="install_check_again">दोबारा जाँचें</string>
<string name="install_continue_anyway">फिर भी जारी रखें</string>
<string name="install_onboarding">Samsung Health Monitor खोलें और उसका सेटअप पूरा करें</string>
Expand Down
Loading
Loading