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
33 changes: 25 additions & 8 deletions lib/ble/ble_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2796,17 +2796,17 @@ class BleEngine {
bondRefusals: state.bondRefusals,
);

/// Read the adapter state without hanging: `unknown` is the pre-init value and
/// never a verdict, so wait past it, but never longer than [_blockerProbe].
/// Read past pre-init/resetting states without an unbounded startup await.
static const Duration _blockerProbe = Duration(seconds: 2);

Future<String> _readAdapterState() => waitForBleAdapterState(
FlutterBluePlus.adapterState.map((s) => s.name),
timeout: _blockerProbe,
);

Future<BleBlocker?> _detectBlocker() async {
try {
final s = await FlutterBluePlus.adapterState
.firstWhere((s) => s != BluetoothAdapterState.unknown)
.timeout(_blockerProbe,
onTimeout: () => BluetoothAdapterState.unknown);
return classifyBleBlocker(adapterState: s.name);
return classifyBleBlocker(adapterState: await _readAdapterState());
} catch (e) {
return classifyBleBlocker(error: e);
}
Expand All @@ -2819,7 +2819,7 @@ class BleEngine {
onState(state);
}

/// Anything that actually reached the radio proves the stack is usable again.
/// Remove a previous blocker once current evidence no longer supports it.
void _clearBlocker() {
if (_blocker == null) return;
_blocker = null;
Expand Down Expand Up @@ -2920,6 +2920,23 @@ class BleEngine {
);

try {
// ASK authorizes the accessory before FBP creates its own central. That
// central initially reports Unknown even with the radio on. Calling
// connect immediately makes Darwin reject it with generic "Bluetooth
// must be turned on" text, forcing an unnecessary user retry.
final adapter = await _readAdapterState();
if (adapter != 'on') {
final blocker = classifyBleBlocker(adapterState: adapter);
if (blocker != null) {
_noteBlocker(blocker);
} else {
_clearBlocker();
}
_log('[BLE] connect not started: adapter state is $adapter.');
await _failConnect();
return false;
}
_clearBlocker();
await device.connect(
timeout: const Duration(seconds: 20),
autoConnect: false,
Expand Down
37 changes: 37 additions & 0 deletions lib/ble/ble_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,34 @@ class BleUnavailableException implements Exception {
String toString() => 'BleUnavailableException(${blocker.name})';
}

/// Wait past the adapter's initial/transition states before issuing a radio
/// command. A deadline or an ended stream leaves the state unknown; neither
/// proves that Bluetooth is off. State names keep this independent of FBP.
Future<String> waitForBleAdapterState(
Stream<String> states, {
required Duration timeout,
}) async {
final result = Completer<String>();
final sub = states.listen((state) {
if (state != 'unknown' && state != 'turningOn' && !result.isCompleted) {
result.complete(state);
}
}, onError: (Object error, StackTrace stack) {
if (!result.isCompleted) result.completeError(error, stack);
}, onDone: () {
if (!result.isCompleted) result.complete('unknown');
});
final deadline = Timer(timeout, () {
if (!result.isCompleted) result.complete('unknown');
});
try {
return await result.future;
} finally {
deadline.cancel();
await sub.cancel();
}
}

/// Map an adapter-state name and/or a thrown transport error onto a blocker.
/// Returns null when neither says the stack is unusable — i.e. the failure is
/// about the band, not the phone.
Expand All @@ -124,6 +152,15 @@ BleBlocker? classifyBleBlocker({String? adapterState, Object? error}) {
}
if (error == null) return null;
final s = error.toString().toLowerCase();
// FBP's Darwin plugin uses "bluetooth must be turned on" for EVERY
// CBManager state other than PoweredOn, including its initial Unknown.
// Prefer the native state it includes over that generic wording.
if (s.contains('cbmanagerstateunknown') ||
s.contains('cbmanagerstateresetting') ||
s.contains('cbmanagerstatepoweredon')) {
return null;
}
if (s.contains('cbmanagerstateunsupported')) return BleBlocker.unsupported;
if (s.contains('unauthorized') ||
s.contains('permission') ||
s.contains('not authorized') ||
Expand Down
8 changes: 6 additions & 2 deletions lib/data/db.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2280,12 +2280,14 @@ class LocalDb {
'UPDATE device SET wearing = ?, wearing_set_ts = ? '
'WHERE id = ? AND wearing_set_ts IS NULL';

/// [executor] includes adoption in the caller's pairing transaction.
static Future<int> adoptDeviceWearing(
int wearing,
int setTs, [
String id = kPrimaryDeviceId,
DatabaseExecutor? executor,
]) async {
final db = await instance;
final db = executor ?? await instance;
return db.rawUpdate(_adoptWearingSql, [wearing, setTs, id]);
}

Expand Down Expand Up @@ -3135,15 +3137,17 @@ class LocalDb {
if (n > 0) deviceRowChanged.add(id);
}

/// [executor] includes the upsert in the caller's persistence transaction.
static Future<void> upsertDevice({
String id = kPrimaryDeviceId,
String? adapterId,
String? remoteId,
String? label,
String? tier,
bool clearAdapterId = false,
DatabaseExecutor? executor,
}) async {
final db = await instance;
final db = executor ?? await instance;
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
await db.insert('device', {
'id': id,
Expand Down
6 changes: 3 additions & 3 deletions lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"pillLocalNoCloud": "Lokal · keine Cloud",
"pairingSkipForNow": "Vorerst überspringen",
"pairingSkipNote": "Die App startet ohne Band. Gemessen wird erst, wenn eines gekoppelt ist.",
"pairingIdleTitle": "Band aufwecken und nah heranhalten",
"pairingIdleBody": "Nimm das Band vom Ladegerät, leg es ans Handgelenk und halte dein Smartphone in Reichweite.",
"pairingIdleTitle": "Versetze dein Band in den Kopplungsmodus",
"pairingIdleBody": "Nimm das Band vom Handgelenk und tippe kräftig auf die Oberseite, bis es blau blinkt. Halte dein Smartphone in der Nähe.",
"pairingScanningTitle": "Suche nach deinem Band",
"pairingScanningBody": "Ein Band, das gerade vom Ladegerät kommt, braucht bis zu einer halben Minute, bis es sichtbar ist.",
"pairingNotFoundTitle": "Kein Band in Reichweite",
Expand All @@ -42,7 +42,7 @@
"pairingBondRefusedAdviceBody": "Öffne die Bluetooth-Einstellungen deines Smartphones, entferne das Band und suche anschließend erneut. Der abgelehnte Schlüssel gehört zu einer alten Kopplung und kann nur über das System gelöscht werden.",
"pairingBondRefusedAdviceFix": "Dann zurückkommen und auf „Erneut versuchen“ tippen",
"pairingFailedAdviceTitle": "Band gefunden, aber Verbindung nicht abgeschlossen",
"pairingFailedAdviceBody": "Meist klappt es, wenn du aus etwa einem Meter Entfernung erneut suchst.",
"pairingFailedAdviceBody": "Versetze das Band in den Kopplungsmodus (blaues Blinken) und tippe dann auf „Erneut versuchen“.",
"profileSetupTitle": "Über dich",
"profileSetupBody": "Das Geschlecht ist die einzige Angabe, die zum Fortfahren nötig ist. Alter, Größe und Gewicht beeinflussen, wie deine Daten ausgewertet werden; lässt du eine davon leer, bleiben nur die Werte aus, die sie benötigen.",
"profileSetupSexHeader": "GESCHLECHT",
Expand Down
6 changes: 3 additions & 3 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@
"@pairingSkipNote": {
"description": "Note under the skip button."
},
"pairingIdleTitle": "Wake the band and hold it close",
"pairingIdleTitle": "Put your band in pairing mode",
"@pairingIdleTitle": {
"description": "Pairing state title: idle."
},
"pairingIdleBody": "Take the band off the charger, put it on your wrist and keep the phone within arm’s reach.",
"pairingIdleBody": "Take the band off your wrist and tap the top firmly until it flashes blue. Keep your phone nearby.",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
"@pairingIdleBody": {
"description": "Pairing state body: idle."
},
Expand Down Expand Up @@ -168,7 +168,7 @@
"@pairingFailedAdviceTitle": {
"description": "Advice card title: failed."
},
"pairingFailedAdviceBody": "Scanning again from a metre away normally works.",
"pairingFailedAdviceBody": "Put the band in pairing mode (flashing blue), then tap Try again.",
"@pairingFailedAdviceBody": {
"description": "Advice card body: failed."
},
Expand Down
6 changes: 3 additions & 3 deletions lib/l10n/app_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"pillLocalNoCloud": "Local · sin nube",
"pairingSkipForNow": "Omitir por ahora",
"pairingSkipNote": "La app se abre sin banda. No se mide nada hasta que se empareje una.",
"pairingIdleTitle": "Despierta la banda y acércala",
"pairingIdleBody": "Quita la banda del cargador, póntela en la muñeca y mantén el teléfono cerca.",
"pairingIdleTitle": "Pon tu banda en modo de emparejamiento",
"pairingIdleBody": "Quítate la banda de la muñeca y toca con firmeza la parte superior hasta que parpadee en azul. Mantén el teléfono cerca.",
"pairingScanningTitle": "Buscando tu banda",
"pairingScanningBody": "Una banda recién sacada del cargador puede tardar hasta medio minuto en empezar a anunciarse.",
"pairingNotFoundTitle": "No hay banda al alcance",
Expand All @@ -42,7 +42,7 @@
"pairingBondRefusedAdviceBody": "Abre los ajustes de Bluetooth del teléfono, olvida la banda y vuelve a buscar aquí. La clave rechazada es el registro de emparejamiento antiguo, y solo el sistema puede borrarlo.",
"pairingBondRefusedAdviceFix": "Luego vuelve y toca Intentar de nuevo",
"pairingFailedAdviceTitle": "Se encontró la banda pero la sesión no terminó",
"pairingFailedAdviceBody": "Volver a buscar desde un metro de distancia suele funcionar.",
"pairingFailedAdviceBody": "Pon la banda en modo de emparejamiento (parpadeo azul) y toca «Intentar de nuevo».",
"profileSetupTitle": "Sobre ti",
"profileSetupBody": "El sexo es la única opción obligatoria para continuar. La edad, la altura y el peso cambian cómo se puntúan tus datos; deja alguno en blanco y solo las métricas que lo necesiten quedarán sin disponibilidad.",
"profileSetupSexHeader": "SEXO",
Expand Down
6 changes: 3 additions & 3 deletions lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"pillLocalNoCloud": "Local · sans cloud",
"pairingSkipForNow": "Ignorer pour l’instant",
"pairingSkipNote": "L’application s’ouvre sans bracelet. Rien n’est mesuré tant qu’aucun n’est jumelé.",
"pairingIdleTitle": "Réveillez le bracelet et rapprochez-le",
"pairingIdleBody": "Retirez le bracelet du chargeur, mettez-le à votre poignet et gardez le téléphone à portée de main.",
"pairingIdleTitle": "Mettez votre bracelet en mode jumelage",
"pairingIdleBody": "Retirez le bracelet de votre poignet et tapotez fermement le dessus jusqu’à ce qu’il clignote en bleu. Gardez votre téléphone à proximité.",
"pairingScanningTitle": "Recherche de votre bracelet",
"pairingScanningBody": "Un bracelet qui vient d’être retiré du chargeur peut mettre jusqu’à trente secondes avant de se signaler.",
"pairingNotFoundTitle": "Aucun bracelet à portée",
Expand All @@ -42,7 +42,7 @@
"pairingBondRefusedAdviceBody": "Ouvrez les réglages Bluetooth du téléphone, oubliez le bracelet, puis relancez la recherche ici. La clé refusée est l’ancien enregistrement de jumelage, et seul le système peut l’effacer.",
"pairingBondRefusedAdviceFix": "Revenez ensuite et touchez Réessayer",
"pairingFailedAdviceTitle": "Le bracelet a été trouvé mais la session ne s’est pas terminée",
"pairingFailedAdviceBody": "Relancer la recherche à un mètre de distance fonctionne généralement.",
"pairingFailedAdviceBody": "Mettez le bracelet en mode jumelage (clignotement bleu), puis appuyez sur Réessayer.",
"profileSetupTitle": "À propos de vous",
"profileSetupBody": "Le sexe est le seul choix obligatoire pour continuer. L'âge, la taille et le poids modifient la façon dont vos données sont notées ; laissez-en un vide et seules les métriques qui en ont besoin resteront indisponibles.",
"profileSetupSexHeader": "SEXE",
Expand Down
6 changes: 3 additions & 3 deletions lib/l10n/app_hi.arb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"pillLocalNoCloud": "स्थानीय · कोई क्लाउड नहीं",
"pairingSkipForNow": "अभी के लिए छोड़ें",
"pairingSkipNote": "ऐप बिना बैंड के खुलता है। जब तक कोई बैंड जोड़ा नहीं जाता, कुछ भी मापा नहीं जाता।",
"pairingIdleTitle": "बैंड को जगाएं और पास रखें",
"pairingIdleBody": "बैंड को चार्जर से हटाएं, कलाई पर पहनें और फ़ोन को पास रखें।",
"pairingIdleTitle": "अपने बैंड को पेयरिंग मोड में डालें",
"pairingIdleBody": "बैंड को कलाई से उतारें और उसके ऊपरी हिस्से को ज़ोर से थपथपाएँ, जब तक नीली रोशनी न झपकने लगे। फ़ोन को पास रखें।",
"pairingScanningTitle": "आपका बैंड खोजा जा रहा है",
"pairingScanningBody": "अभी चार्जर से हटाया गया बैंड सिग्नल भेजना शुरू करने में आधा मिनट तक ले सकता है।",
"pairingNotFoundTitle": "सीमा में कोई बैंड नहीं",
Expand All @@ -42,7 +42,7 @@
"pairingBondRefusedAdviceBody": "फ़ोन की ब्लूटूथ सेटिंग्स खोलें, बैंड को भूलें, फिर यहां दोबारा स्कैन करें। अस्वीकृत कुंजी पुराना पेयरिंग रिकॉर्ड है, और केवल सिस्टम ही इसे साफ़ कर सकता है।",
"pairingBondRefusedAdviceFix": "फिर वापस आकर फिर से प्रयास करें पर टैप करें",
"pairingFailedAdviceTitle": "बैंड मिल गया लेकिन सत्र पूरा नहीं हुआ",
"pairingFailedAdviceBody": "एक मीटर की दूरी से दोबारा स्कैन करना आमतौर पर काम करता है।",
"pairingFailedAdviceBody": "बैंड को पेयरिंग मोड में डालें (नीली रोशनी झपकती है), फिर «फिर से प्रयास करें» पर टैप करें।",
"profileSetupTitle": "आपके बारे में",
"profileSetupBody": "आगे बढ़ने के लिए केवल लिंग चुनना ज़रूरी है। उम्र, लंबाई और वज़न आपके डेटा के स्कोर होने के तरीके को बदलते हैं; इनमें से किसी को भी खाली छोड़ें, केवल उसकी आवश्यकता वाले मेट्रिक्स ही अनुपलब्ध रहेंगे।",
"profileSetupSexHeader": "लिंग",
Expand Down
6 changes: 3 additions & 3 deletions lib/l10n/app_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"pillLocalNoCloud": "Локально · без облака",
"pairingSkipForNow": "Пропустить",
"pairingSkipNote": "Приложение откроется без браслета. Пока браслет не подключён, измерения выполняться не будут.",
"pairingIdleTitle": "Разбудите браслет и держите его рядом",
"pairingIdleBody": "Снимите браслет с зарядки, наденьте на запястье и держите телефон на расстоянии вытянутой руки.",
"pairingIdleTitle": "Переведите браслет в режим сопряжения",
"pairingIdleBody": "Снимите браслет с запястья и уверенно постучите по верхней части, пока он не начнёт мигать синим. Держите телефон рядом.",
"pairingScanningTitle": "Ищем браслет",
"pairingScanningBody": "После снятия с зарядки браслету может потребоваться до 30 секунд, чтобы начать передавать сигнал.",
"pairingNotFoundTitle": "Браслет не найден",
Expand All @@ -41,7 +41,7 @@
"pairingBondRefusedAdviceBody": "В настройках Bluetooth телефона забудьте браслет и снова запустите поиск здесь. Старую запись о сопряжении может удалить только система.",
"pairingBondRefusedAdviceFix": "Затем вернитесь и нажмите «Повторить»",
"pairingFailedAdviceTitle": "Браслет найден, но подключение не завершилось",
"pairingFailedAdviceBody": "Обычно помогает повторный поиск с расстояния около метра.",
"pairingFailedAdviceBody": "Переведите браслет в режим сопряжения (мигающий синий индикатор), затем нажмите «Повторить».",
"profileSetupTitle": "О вас",
"profileSetupBody": "Чтобы продолжить, нужно указать только пол. Возраст, рост и вес влияют на расчёт ваших показателей; если оставить какой-либо из них пустым, недоступными останутся только зависящие от него показатели.",
"profileSetupSexHeader": "ПОЛ",
Expand Down
6 changes: 3 additions & 3 deletions lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"pillLocalNoCloud": "本地 · 无云端",
"pairingSkipForNow": "暂时跳过",
"pairingSkipNote": "应用将在没有腕带的情况下打开。配对之前不会测量任何数据。",
"pairingIdleTitle": "唤醒腕带并靠近手机",
"pairingIdleBody": "将腕带从充电器上取下,戴在手腕上,并将手机放在触手可及的地方。",
"pairingIdleTitle": "将腕带置于配对模式",
"pairingIdleBody": "从手腕上取下腕带,用力轻拍顶部,直到蓝灯闪烁。请将手机放在附近。",
"pairingScanningTitle": "正在寻找您的腕带",
"pairingScanningBody": "刚从充电器上取下的腕带最多可能需要半分钟才能开始广播。",
"pairingNotFoundTitle": "范围内未找到腕带",
Expand All @@ -42,7 +42,7 @@
"pairingBondRefusedAdviceBody": "打开手机的蓝牙设置,忘记该腕带,然后在此重新扫描。被拒绝的密钥是旧的配对记录,只有系统才能清除它。",
"pairingBondRefusedAdviceFix": "然后返回并点按“重试”",
"pairingFailedAdviceTitle": "找到了腕带,但会话未完成",
"pairingFailedAdviceBody": "从一米距离再次扫描通常有效。",
"pairingFailedAdviceBody": "将腕带置于配对模式(蓝灯闪烁),然后点按“重试”。",
"profileSetupTitle": "关于你",
"profileSetupBody": "继续前只需选择性别。年龄、身高和体重会影响你的数据评分方式;留空其中任何一个,只有需要它的指标会不可用。",
"profileSetupSexHeader": "性别",
Expand Down
Loading
Loading