@_nazmiforreal/flutter-ota 0.1.29 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flutter-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +0 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +754 -0
- package/dart-src/packages/cli-tools/.dart_tool/version +1 -0
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +22 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +17 -8
- package/dart-src/packages/cli-tools/pubspec.lock +636 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +6 -6
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +2 -1
- package/dart-src/root/analysis_options.yaml +3 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherPlugin.kt +1 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfig.kt +20 -0
- package/dart-src/root/lib/flutter_ota_kit.dart +31 -11
- package/dart-src/root/lib/src/aws_update_source.dart +1 -0
- package/dart-src/root/lib/src/cloudflare_update_source.dart +1 -0
- package/dart-src/root/lib/src/ota_progress_overlay.dart +196 -130
- package/dart-src/root/lib/src/patcher_channel.dart +14 -1
- package/dart-src/root/lib/src/pkg/plugin_core/src/request_update_bundle_state.dart +2 -1
- package/dart-src/root/lib/src/pkg/postgres/src/postgres_types.dart +2 -1
- package/dart-src/root/lib/src/pocketbase_update_source.dart +1 -0
- package/dart-src/root/lib/src/postgres_update_source.dart +1 -0
- package/dart-src/root/lib/src/shared_update_check.dart +30 -2
- package/dart-src/root/lib/src/supabase_update_source.dart +1 -0
- package/dart-src/root/pubspec.yaml +14 -14
- package/package.json +1 -1
package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfig.kt
CHANGED
|
@@ -30,6 +30,11 @@ internal object PatcherConfig {
|
|
|
30
30
|
const val KEY_CRASH_COUNT = "crash_count"
|
|
31
31
|
const val KEY_PATCH_LOADING = "patch_loading"
|
|
32
32
|
|
|
33
|
+
/** Random UUID generated on first read and persisted; seeds the rollout
|
|
34
|
+
* cohort on the Dart side. Survives patch updates and base-APK upgrades —
|
|
35
|
+
* only reset by uninstall / `pm clear`, like the rest of these prefs. */
|
|
36
|
+
private const val KEY_DEVICE_ID = "device_install_id"
|
|
37
|
+
|
|
33
38
|
/** PID written at [com.flutter_patcher.flutter_patcher.CrashGuard.markBooting]; consumed
|
|
34
39
|
* next cold start to look up `ActivityManager.getHistoricalProcessExitReasons` (API 30+).
|
|
35
40
|
* Unused on API < 30 — that path uses the naive "patch_loading=true ⇒ crash" rule. */
|
|
@@ -87,6 +92,21 @@ internal object PatcherConfig {
|
|
|
87
92
|
fun prefs(context: Context): SharedPreferences =
|
|
88
93
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
89
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Stable per-install device identifier (generated once, persisted).
|
|
97
|
+
*
|
|
98
|
+
* Used as the seed for server-side rollout-cohort hashing so a device
|
|
99
|
+
* lands in a consistent cohort across releases. Never leaves the device —
|
|
100
|
+
* the app sends only the derived 0-999 cohort number.
|
|
101
|
+
*/
|
|
102
|
+
fun deviceInstallId(context: Context): String {
|
|
103
|
+
val existing = prefs(context).getString(KEY_DEVICE_ID, null)
|
|
104
|
+
if (!existing.isNullOrEmpty()) return existing
|
|
105
|
+
val fresh = java.util.UUID.randomUUID().toString()
|
|
106
|
+
prefs(context).edit().putString(KEY_DEVICE_ID, fresh).apply()
|
|
107
|
+
return fresh
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
internal fun encodeLoaderFieldCandidates(fields: List<String>): String {
|
|
91
111
|
val normalized = fields
|
|
92
112
|
.map { it.trim() }
|
|
@@ -756,7 +756,9 @@ class FlutterPatcher {
|
|
|
756
756
|
///
|
|
757
757
|
/// Call [restart] after a successful rollback to activate the previous patch.
|
|
758
758
|
static Future<RollbackOutcome> rollbackToPrevious() async {
|
|
759
|
-
if (_notAndroidGuard('rollbackToPrevious'))
|
|
759
|
+
if (_notAndroidGuard('rollbackToPrevious')) {
|
|
760
|
+
return RollbackOutcome.fallbackToBase;
|
|
761
|
+
}
|
|
760
762
|
try {
|
|
761
763
|
final outcomeName = await PatcherChannel.rollbackToPrevious();
|
|
762
764
|
return RollbackOutcome.values.byName(outcomeName);
|
|
@@ -815,8 +817,9 @@ class FlutterPatcher {
|
|
|
815
817
|
final showOverlay = showUpdateUi && result.shouldForceUpdate;
|
|
816
818
|
final fromVersion = await currentVersion;
|
|
817
819
|
final patchHash = result.patch!.md5;
|
|
818
|
-
final shortHash =
|
|
819
|
-
|
|
820
|
+
final shortHash = patchHash.isNotEmpty
|
|
821
|
+
? patchHash.substring(0, patchHash.length.clamp(0, 8))
|
|
822
|
+
: null;
|
|
820
823
|
|
|
821
824
|
// Extract channel/platform from whichever config is active
|
|
822
825
|
String? channel;
|
|
@@ -878,7 +881,9 @@ class FlutterPatcher {
|
|
|
878
881
|
if (error != null &&
|
|
879
882
|
deterministic.contains(error) &&
|
|
880
883
|
patchVersion.isNotEmpty) {
|
|
881
|
-
_log(
|
|
884
|
+
_log(
|
|
885
|
+
'applyUpdate: patch failed (${error.name}), blacklisting $patchVersion',
|
|
886
|
+
);
|
|
882
887
|
try {
|
|
883
888
|
await PatcherChannel.reportApplyFailure(
|
|
884
889
|
version: patchVersion,
|
|
@@ -889,15 +894,22 @@ class FlutterPatcher {
|
|
|
889
894
|
_log('applyUpdate: failed to blacklist $patchVersion: $e', s);
|
|
890
895
|
}
|
|
891
896
|
} else {
|
|
892
|
-
_log(
|
|
893
|
-
|
|
897
|
+
_log(
|
|
898
|
+
'applyUpdate: patch failed (${error?.name}); transient, not '
|
|
899
|
+
'blacklisting (will retry next check)',
|
|
900
|
+
);
|
|
894
901
|
}
|
|
895
902
|
}
|
|
896
903
|
|
|
897
904
|
if (applied.ok && result.shouldForceUpdate) {
|
|
898
905
|
handle?.end();
|
|
899
|
-
//
|
|
900
|
-
|
|
906
|
+
// Let the overlay finish its minimum display time + success dwell and
|
|
907
|
+
// fully unmount before killing the process. The old fixed 3s wait could
|
|
908
|
+
// restart while the install screen was still visible, because a fast
|
|
909
|
+
// download keeps the overlay up until the 7s minimum duration elapses.
|
|
910
|
+
await (handle?.dismissed ??
|
|
911
|
+
Future<void>.delayed(const Duration(seconds: 3)))
|
|
912
|
+
.timeout(const Duration(seconds: 12), onTimeout: () {});
|
|
901
913
|
await restart();
|
|
902
914
|
} else {
|
|
903
915
|
handle?.end(hasError: !applied.ok, errorText: applied.message);
|
|
@@ -932,7 +944,9 @@ class FlutterPatcher {
|
|
|
932
944
|
if (showUpdateUi) {
|
|
933
945
|
OtaOverlayManager.instance.showRollbackToast(
|
|
934
946
|
message: 'Update reverted — using previous version',
|
|
935
|
-
previousVersion: outcome == RollbackOutcome.success
|
|
947
|
+
previousVersion: outcome == RollbackOutcome.success
|
|
948
|
+
? 'previous'
|
|
949
|
+
: 'base',
|
|
936
950
|
);
|
|
937
951
|
}
|
|
938
952
|
await restart();
|
|
@@ -953,7 +967,10 @@ class FlutterPatcher {
|
|
|
953
967
|
final patchVersion = result.patch!.version;
|
|
954
968
|
final patchMd5 = result.patch!.md5;
|
|
955
969
|
try {
|
|
956
|
-
if (await PatcherChannel.isVersionBlacklisted(
|
|
970
|
+
if (await PatcherChannel.isVersionBlacklisted(
|
|
971
|
+
patchVersion,
|
|
972
|
+
md5: patchMd5,
|
|
973
|
+
)) {
|
|
957
974
|
_log('checkAndApplyUpdates: $patchVersion is blacklisted, skip');
|
|
958
975
|
return null;
|
|
959
976
|
}
|
|
@@ -1039,7 +1056,10 @@ class FlutterPatcher {
|
|
|
1039
1056
|
///
|
|
1040
1057
|
/// When [md5] is provided, the match uses both version and md5. Without md5,
|
|
1041
1058
|
/// any entry with the same version is considered a match.
|
|
1042
|
-
static Future<bool> isVersionBlacklisted(
|
|
1059
|
+
static Future<bool> isVersionBlacklisted(
|
|
1060
|
+
String version, {
|
|
1061
|
+
String md5 = '',
|
|
1062
|
+
}) async {
|
|
1043
1063
|
if (_notAndroidGuard('isVersionBlacklisted')) return false;
|
|
1044
1064
|
try {
|
|
1045
1065
|
return await PatcherChannel.isVersionBlacklisted(version, md5: md5);
|
|
@@ -275,36 +275,35 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
275
275
|
|
|
276
276
|
// ── Title bar ─────────────────────────────────────────────────────────
|
|
277
277
|
Widget _titleBar(OtaOverlayState s) => Container(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
278
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
|
|
279
|
+
decoration: const BoxDecoration(
|
|
280
|
+
color: _C.head,
|
|
281
|
+
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
|
282
|
+
border: Border(bottom: BorderSide(color: _C.border)),
|
|
283
|
+
),
|
|
284
|
+
child: Row(
|
|
285
|
+
children: [
|
|
286
|
+
_dot(_C.red),
|
|
287
|
+
const SizedBox(width: 6),
|
|
288
|
+
_dot(_C.yellow),
|
|
289
|
+
const SizedBox(width: 6),
|
|
290
|
+
_dot(_C.green),
|
|
291
|
+
const SizedBox(width: 12),
|
|
292
|
+
const Flexible(
|
|
293
|
+
child: Text(
|
|
294
|
+
'flutter-ota · forced update',
|
|
295
|
+
overflow: TextOverflow.ellipsis,
|
|
296
|
+
style: TextStyle(color: _C.text, fontFamily: _mono, fontSize: 12),
|
|
297
|
+
),
|
|
283
298
|
),
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
_dot(_C.yellow),
|
|
289
|
-
const SizedBox(width: 6),
|
|
290
|
-
_dot(_C.green),
|
|
291
|
-
const SizedBox(width: 12),
|
|
292
|
-
const Flexible(
|
|
293
|
-
child: Text(
|
|
294
|
-
'flutter-ota · forced update',
|
|
295
|
-
overflow: TextOverflow.ellipsis,
|
|
296
|
-
style: TextStyle(
|
|
297
|
-
color: _C.text, fontFamily: _mono, fontSize: 12),
|
|
298
|
-
),
|
|
299
|
-
),
|
|
300
|
-
const SizedBox(width: 8),
|
|
301
|
-
_pill(
|
|
302
|
-
s.hasError ? 'FAILED' : 'RUNNING',
|
|
303
|
-
s.hasError ? _C.red : _C.green,
|
|
304
|
-
),
|
|
305
|
-
],
|
|
299
|
+
const SizedBox(width: 8),
|
|
300
|
+
_pill(
|
|
301
|
+
s.hasError ? 'FAILED' : 'RUNNING',
|
|
302
|
+
s.hasError ? _C.red : _C.green,
|
|
306
303
|
),
|
|
307
|
-
|
|
304
|
+
],
|
|
305
|
+
),
|
|
306
|
+
);
|
|
308
307
|
|
|
309
308
|
// ── META ──────────────────────────────────────────────────────────────
|
|
310
309
|
Widget _metaBlock(OtaOverlayState s) {
|
|
@@ -330,12 +329,15 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
330
329
|
style: const TextStyle(fontFamily: _mono, fontSize: 13),
|
|
331
330
|
children: [
|
|
332
331
|
TextSpan(
|
|
333
|
-
|
|
334
|
-
|
|
332
|
+
text: '${r[0].padRight(9)} ',
|
|
333
|
+
style: const TextStyle(color: _C.dim),
|
|
334
|
+
),
|
|
335
335
|
TextSpan(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
336
|
+
text: r[1],
|
|
337
|
+
style: TextStyle(
|
|
338
|
+
color: r[0] == 'version' ? _C.blue : _C.text,
|
|
339
|
+
),
|
|
340
|
+
),
|
|
339
341
|
],
|
|
340
342
|
),
|
|
341
343
|
),
|
|
@@ -367,18 +369,18 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
367
369
|
final Color color = failed
|
|
368
370
|
? _C.red
|
|
369
371
|
: done
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
372
|
+
? _C.green
|
|
373
|
+
: active
|
|
374
|
+
? _C.blue
|
|
375
|
+
: _C.faint;
|
|
374
376
|
|
|
375
377
|
final String glyph = failed
|
|
376
378
|
? '✗'
|
|
377
379
|
: done
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
380
|
+
? '✓'
|
|
381
|
+
: active
|
|
382
|
+
? spinner
|
|
383
|
+
: '·';
|
|
382
384
|
|
|
383
385
|
// Only the downloading step shows a percentage. Verify / install /
|
|
384
386
|
// Only the download step shows a percentage. Other active phases show
|
|
@@ -390,8 +392,8 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
390
392
|
} else if (done) {
|
|
391
393
|
status = 'done';
|
|
392
394
|
} else if (active) {
|
|
393
|
-
final showPct =
|
|
394
|
-
s.fraction != null;
|
|
395
|
+
final showPct =
|
|
396
|
+
s.phase == PatchApplyPhase.downloading && s.fraction != null;
|
|
395
397
|
status = showPct ? _pct(s.fraction) : 'working…';
|
|
396
398
|
} else {
|
|
397
399
|
status = 'wait';
|
|
@@ -403,9 +405,14 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
403
405
|
children: [
|
|
404
406
|
SizedBox(
|
|
405
407
|
width: 18,
|
|
406
|
-
child: Text(
|
|
407
|
-
|
|
408
|
-
|
|
408
|
+
child: Text(
|
|
409
|
+
glyph,
|
|
410
|
+
style: TextStyle(
|
|
411
|
+
color: color,
|
|
412
|
+
fontFamily: _mono,
|
|
413
|
+
fontSize: 14,
|
|
414
|
+
),
|
|
415
|
+
),
|
|
409
416
|
),
|
|
410
417
|
const SizedBox(width: 8),
|
|
411
418
|
Expanded(
|
|
@@ -441,8 +448,8 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
441
448
|
// Single honest fraction for the bar:
|
|
442
449
|
// - preparing (no phase / no fraction yet) → empty
|
|
443
450
|
// - downloading with a known size → real fraction
|
|
444
|
-
// - verifying /
|
|
445
|
-
//
|
|
451
|
+
// - verifying / finalizing / restarting → full (download is already
|
|
452
|
+
// done, so the bar rests at 100%)
|
|
446
453
|
// - error → freeze wherever it was
|
|
447
454
|
final double frac;
|
|
448
455
|
if (s.hasError) {
|
|
@@ -458,32 +465,51 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
458
465
|
frac = 0.0; // preparing
|
|
459
466
|
}
|
|
460
467
|
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
468
|
+
// The readout lives on its own line above the bar (right aligned) so the
|
|
469
|
+
// bar itself spans the full panel width and can visibly reach the end.
|
|
470
|
+
// It used to sit in a fixed column beside the bar, which (a) reserved
|
|
471
|
+
// 54px the bar could never grow into and (b) hid the percent as soon as
|
|
472
|
+
// the download phase ended, making the bar look stuck below 100%.
|
|
473
|
+
final String pctLabel;
|
|
474
|
+
if (s.hasError) {
|
|
475
|
+
pctLabel = '';
|
|
476
|
+
} else if (s.restarting ||
|
|
477
|
+
s.phase == PatchApplyPhase.verifying ||
|
|
478
|
+
s.phase == PatchApplyPhase.finalizing) {
|
|
479
|
+
pctLabel = _pct(1.0);
|
|
480
|
+
} else if (s.phase == PatchApplyPhase.downloading && s.fraction != null) {
|
|
481
|
+
pctLabel = _pct(s.fraction);
|
|
482
|
+
} else {
|
|
483
|
+
pctLabel = '';
|
|
484
|
+
}
|
|
464
485
|
|
|
465
486
|
return _panelBox(
|
|
466
|
-
child:
|
|
487
|
+
child: Column(
|
|
488
|
+
key: const Key('ota-progress-panel'),
|
|
489
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
467
490
|
children: [
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
491
|
+
SizedBox(
|
|
492
|
+
height: 16,
|
|
493
|
+
child: Align(
|
|
494
|
+
alignment: Alignment.centerRight,
|
|
495
|
+
child: Text(
|
|
496
|
+
pctLabel,
|
|
497
|
+
style: const TextStyle(
|
|
498
|
+
fontFamily: _mono,
|
|
499
|
+
fontSize: 13,
|
|
500
|
+
color: _C.text,
|
|
501
|
+
fontWeight: FontWeight.w700,
|
|
502
|
+
fontFeatures: [FontFeature.tabularFigures()],
|
|
503
|
+
),
|
|
504
|
+
),
|
|
472
505
|
),
|
|
473
506
|
),
|
|
474
|
-
const SizedBox(
|
|
507
|
+
const SizedBox(height: 4),
|
|
475
508
|
SizedBox(
|
|
476
|
-
|
|
477
|
-
child:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
style: const TextStyle(
|
|
481
|
-
fontFamily: _mono,
|
|
482
|
-
fontSize: 13,
|
|
483
|
-
color: _C.text,
|
|
484
|
-
fontWeight: FontWeight.w700,
|
|
485
|
-
fontFeatures: [FontFeature.tabularFigures()],
|
|
486
|
-
),
|
|
509
|
+
key: const Key('ota-progress-bar'),
|
|
510
|
+
child: _ProgressBar(
|
|
511
|
+
fraction: frac,
|
|
512
|
+
color: s.hasError ? _C.red : _C.blue,
|
|
487
513
|
),
|
|
488
514
|
),
|
|
489
515
|
],
|
|
@@ -522,7 +548,10 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
522
548
|
child: Text(
|
|
523
549
|
s.errorHint ?? 'Close the app and reopen to retry.',
|
|
524
550
|
style: const TextStyle(
|
|
525
|
-
|
|
551
|
+
color: _C.dim,
|
|
552
|
+
fontFamily: _mono,
|
|
553
|
+
fontSize: 12,
|
|
554
|
+
),
|
|
526
555
|
),
|
|
527
556
|
),
|
|
528
557
|
if (showRetry)
|
|
@@ -530,25 +559,35 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
530
559
|
onTap: widget.onRetry,
|
|
531
560
|
child: Container(
|
|
532
561
|
padding: const EdgeInsets.symmetric(
|
|
533
|
-
|
|
562
|
+
horizontal: 14,
|
|
563
|
+
vertical: 7,
|
|
564
|
+
),
|
|
534
565
|
decoration: BoxDecoration(
|
|
535
566
|
borderRadius: BorderRadius.circular(7),
|
|
536
567
|
border: Border.all(color: _C.border),
|
|
537
568
|
),
|
|
538
|
-
child: const Text(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
569
|
+
child: const Text(
|
|
570
|
+
'[ retry ]',
|
|
571
|
+
style: TextStyle(
|
|
572
|
+
color: _C.blue,
|
|
573
|
+
fontFamily: _mono,
|
|
574
|
+
fontSize: 13,
|
|
575
|
+
),
|
|
576
|
+
),
|
|
543
577
|
),
|
|
544
578
|
),
|
|
545
579
|
],
|
|
546
580
|
)
|
|
547
581
|
: Row(
|
|
548
582
|
children: [
|
|
549
|
-
const Text(
|
|
550
|
-
|
|
551
|
-
|
|
583
|
+
const Text(
|
|
584
|
+
'›',
|
|
585
|
+
style: TextStyle(
|
|
586
|
+
color: _C.green,
|
|
587
|
+
fontFamily: _mono,
|
|
588
|
+
fontSize: 13,
|
|
589
|
+
),
|
|
590
|
+
),
|
|
552
591
|
const SizedBox(width: 8),
|
|
553
592
|
Expanded(
|
|
554
593
|
child: Text(
|
|
@@ -556,7 +595,10 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
556
595
|
? s.message!
|
|
557
596
|
: 'Applying update, please keep the app open…',
|
|
558
597
|
style: const TextStyle(
|
|
559
|
-
|
|
598
|
+
color: _C.dim,
|
|
599
|
+
fontFamily: _mono,
|
|
600
|
+
fontSize: 12,
|
|
601
|
+
),
|
|
560
602
|
),
|
|
561
603
|
),
|
|
562
604
|
],
|
|
@@ -566,52 +608,57 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
566
608
|
|
|
567
609
|
// ── helpers ─────────────────────────────────────────────────────────
|
|
568
610
|
Widget _sectionLabel(String t) => Row(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
)
|
|
611
|
+
children: [
|
|
612
|
+
Text(
|
|
613
|
+
t,
|
|
614
|
+
style: const TextStyle(
|
|
615
|
+
color: _C.dim,
|
|
616
|
+
fontFamily: _mono,
|
|
617
|
+
fontSize: 11,
|
|
618
|
+
fontWeight: FontWeight.w700,
|
|
619
|
+
letterSpacing: 1.5,
|
|
620
|
+
),
|
|
621
|
+
),
|
|
622
|
+
const SizedBox(width: 10),
|
|
623
|
+
const Expanded(child: Divider(color: _C.sep, height: 1)),
|
|
624
|
+
],
|
|
625
|
+
);
|
|
582
626
|
|
|
583
627
|
Widget _panelBox({required Widget child}) => Container(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
628
|
+
width: double.infinity,
|
|
629
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
630
|
+
decoration: BoxDecoration(
|
|
631
|
+
color: _C.inner,
|
|
632
|
+
borderRadius: BorderRadius.circular(8),
|
|
633
|
+
border: Border.all(color: _C.sep),
|
|
634
|
+
),
|
|
635
|
+
child: child,
|
|
636
|
+
);
|
|
593
637
|
|
|
594
638
|
Widget _dot(Color c) => Container(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
639
|
+
width: 11,
|
|
640
|
+
height: 11,
|
|
641
|
+
decoration: BoxDecoration(color: c, shape: BoxShape.circle),
|
|
642
|
+
);
|
|
599
643
|
|
|
600
644
|
Widget _pill(String t, Color c) => Container(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
645
|
+
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
|
|
646
|
+
decoration: BoxDecoration(
|
|
647
|
+
color: c.withValues(alpha: 0.15),
|
|
648
|
+
borderRadius: BorderRadius.circular(6),
|
|
649
|
+
border: Border.all(color: c.withValues(alpha: 0.5)),
|
|
650
|
+
),
|
|
651
|
+
child: Text(
|
|
652
|
+
t,
|
|
653
|
+
style: TextStyle(
|
|
654
|
+
color: c,
|
|
655
|
+
fontFamily: _mono,
|
|
656
|
+
fontSize: 10,
|
|
657
|
+
fontWeight: FontWeight.w700,
|
|
658
|
+
letterSpacing: 0.5,
|
|
659
|
+
),
|
|
660
|
+
),
|
|
661
|
+
);
|
|
615
662
|
}
|
|
616
663
|
|
|
617
664
|
/// A single-line determinate progress bar that never wraps.
|
|
@@ -742,19 +789,20 @@ class _TerminalLogState extends State<_TerminalLog> {
|
|
|
742
789
|
style: const TextStyle(fontFamily: _mono, fontSize: 12),
|
|
743
790
|
children: [
|
|
744
791
|
TextSpan(
|
|
745
|
-
|
|
746
|
-
|
|
792
|
+
text: '${match.group(1)!.padRight(8)} ',
|
|
793
|
+
style: TextStyle(color: color),
|
|
794
|
+
),
|
|
747
795
|
TextSpan(
|
|
748
|
-
|
|
749
|
-
|
|
796
|
+
text: match.group(3),
|
|
797
|
+
style: const TextStyle(color: _C.text),
|
|
798
|
+
),
|
|
750
799
|
],
|
|
751
800
|
),
|
|
752
801
|
);
|
|
753
802
|
} else {
|
|
754
803
|
content = Text(
|
|
755
804
|
line.text,
|
|
756
|
-
style: TextStyle(
|
|
757
|
-
fontFamily: _mono, fontSize: 12, color: color),
|
|
805
|
+
style: TextStyle(fontFamily: _mono, fontSize: 12, color: color),
|
|
758
806
|
);
|
|
759
807
|
}
|
|
760
808
|
|
|
@@ -781,6 +829,14 @@ class OtaOverlayManager {
|
|
|
781
829
|
bool _disposed = false;
|
|
782
830
|
DateTime? _startTime;
|
|
783
831
|
|
|
832
|
+
/// Completes once the active overlay entry has been removed. [begin] resets
|
|
833
|
+
/// it for every session; `FlutterPatcher.applyUpdate` awaits it so the
|
|
834
|
+
/// forced-update restart can never kill a still-visible install screen.
|
|
835
|
+
Completer<void>? _dismissed;
|
|
836
|
+
|
|
837
|
+
Future<void> get _dismissedFuture =>
|
|
838
|
+
_dismissed?.future ?? Future<void>.value();
|
|
839
|
+
|
|
784
840
|
/// Last 10%-bucket we emitted a "download NN%" log line for, to throttle the
|
|
785
841
|
/// feed. -1 = none yet.
|
|
786
842
|
int _lastLoggedPctBucket = -1;
|
|
@@ -820,6 +876,7 @@ class OtaOverlayManager {
|
|
|
820
876
|
|
|
821
877
|
_startTime = DateTime.now();
|
|
822
878
|
_lastLoggedPctBucket = -1;
|
|
879
|
+
_dismissed = Completer<void>();
|
|
823
880
|
|
|
824
881
|
final logs = <LogLine>[
|
|
825
882
|
if (channel != null) LogLine('gray', 'channel $channel'),
|
|
@@ -943,7 +1000,12 @@ class OtaOverlayManager {
|
|
|
943
1000
|
}
|
|
944
1001
|
|
|
945
1002
|
void _end({bool hasError = false, String? errorText}) {
|
|
946
|
-
if (_disposed || _entry == null)
|
|
1003
|
+
if (_disposed || _entry == null) {
|
|
1004
|
+
if (_dismissed != null && !_dismissed!.isCompleted) {
|
|
1005
|
+
_dismissed!.complete();
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
947
1009
|
if (hasError) {
|
|
948
1010
|
_log('red', 'error ${errorText ?? 'unknown'}');
|
|
949
1011
|
_log('yellow', 'action rolling back to base apk');
|
|
@@ -977,6 +1039,9 @@ class OtaOverlayManager {
|
|
|
977
1039
|
if (_disposed) return;
|
|
978
1040
|
entry?.remove();
|
|
979
1041
|
if (identical(_entry, entry)) _entry = null;
|
|
1042
|
+
if (_dismissed != null && !_dismissed!.isCompleted) {
|
|
1043
|
+
_dismissed!.complete();
|
|
1044
|
+
}
|
|
980
1045
|
});
|
|
981
1046
|
}
|
|
982
1047
|
|
|
@@ -1031,10 +1096,7 @@ class OtaOverlayManager {
|
|
|
1031
1096
|
),
|
|
1032
1097
|
Text(
|
|
1033
1098
|
'Restored: $previousVersion',
|
|
1034
|
-
style: TextStyle(
|
|
1035
|
-
color: Colors.grey[400],
|
|
1036
|
-
fontSize: 12,
|
|
1037
|
-
),
|
|
1099
|
+
style: TextStyle(color: Colors.grey[400], fontSize: 12),
|
|
1038
1100
|
),
|
|
1039
1101
|
],
|
|
1040
1102
|
),
|
|
@@ -1064,4 +1126,8 @@ class OtaOverlayHandle {
|
|
|
1064
1126
|
|
|
1065
1127
|
void end({bool hasError = false, String? errorText}) =>
|
|
1066
1128
|
_manager._end(hasError: hasError, errorText: errorText);
|
|
1129
|
+
|
|
1130
|
+
/// Completes when the overlay has fully unmounted (dwell elapsed + removed),
|
|
1131
|
+
/// or immediately if no overlay session is active.
|
|
1132
|
+
Future<void> get dismissed => _manager._dismissedFuture;
|
|
1067
1133
|
}
|
|
@@ -83,6 +83,16 @@ class PatcherChannel {
|
|
|
83
83
|
return channel.invokeMethod<String>('currentVersion');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/// Stable per-install device identifier, persisted natively (SharedPreferences).
|
|
87
|
+
///
|
|
88
|
+
/// Generated once on first call and never changes for the app install (even
|
|
89
|
+
/// across patch updates and base APK upgrades), making it a suitable seed for
|
|
90
|
+
/// rollout cohort hashing. Throws [MissingPluginException] on non-Android
|
|
91
|
+
/// platforms and on Android bases compiled before this method existed.
|
|
92
|
+
static Future<String?> deviceId() async {
|
|
93
|
+
return channel.invokeMethod<String>('deviceId');
|
|
94
|
+
}
|
|
95
|
+
|
|
86
96
|
/// Diagnostics from the last cold-start patch load.
|
|
87
97
|
///
|
|
88
98
|
/// The native side returns `Map?` (see `BootDiagnosticStore`), deserialized by the
|
|
@@ -137,7 +147,10 @@ class PatcherChannel {
|
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
/// Checks whether a patch version (optionally with md5) is blacklisted.
|
|
140
|
-
static Future<bool> isVersionBlacklisted(
|
|
150
|
+
static Future<bool> isVersionBlacklisted(
|
|
151
|
+
String version, {
|
|
152
|
+
String md5 = '',
|
|
153
|
+
}) async {
|
|
141
154
|
final found = await channel.invokeMethod<bool>('isVersionBlacklisted', {
|
|
142
155
|
'version': version,
|
|
143
156
|
'md5': md5,
|