@_nazmiforreal/flutter-ota 0.1.28 → 0.1.29
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.
|
@@ -22,7 +22,7 @@ dependencies:
|
|
|
22
22
|
# merged `flutter_ota_kit` package under lib/src/pkg/. The CLI depends on the
|
|
23
23
|
# single package instead of eight. NOTE: `flutter_ota_kit` is a Flutter plugin,
|
|
24
24
|
# so building the CLI now requires the Flutter SDK (not just the Dart SDK).
|
|
25
|
-
flutter_ota_kit: ^0.2.
|
|
25
|
+
flutter_ota_kit: ^0.2.2
|
|
26
26
|
http: ^1.2.0
|
|
27
27
|
shelf: ^1.4.0
|
|
28
28
|
|
package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatchManager.kt
CHANGED
|
@@ -613,14 +613,22 @@ internal class PatchManager(
|
|
|
613
613
|
}
|
|
614
614
|
|
|
615
615
|
private fun extractBaseAssetsIfNeeded(): Int {
|
|
616
|
-
//
|
|
616
|
+
// Store the base APK's flutter_assets as a single archive for rollback.
|
|
617
|
+
// We keep ONLY the compressed archive (not an uncompressed mirror dir):
|
|
618
|
+
// the previous implementation left both `assets/0/` (uncompressed) AND
|
|
619
|
+
// `assets/0/flutter_assets.apk` (zip) on disk — two full copies of every
|
|
620
|
+
// bundled asset. The uncompressed copy is never read (rollback re-extracts
|
|
621
|
+
// from the archive), so it was pure waste.
|
|
617
622
|
if (!baseAssetsArchive.exists()) {
|
|
618
623
|
baseAssetsArchive.parentFile?.mkdirs()
|
|
624
|
+
val tmp = File(assetsHistoryDir, "0.tmp")
|
|
619
625
|
try {
|
|
620
|
-
copyInstalledFlutterAssets(
|
|
621
|
-
writeFlutterAssetsArchive(
|
|
626
|
+
copyInstalledFlutterAssets(tmp)
|
|
627
|
+
writeFlutterAssetsArchive(tmp, baseAssetsArchive)
|
|
622
628
|
} catch (e: Exception) {
|
|
623
629
|
Log.w(TAG, "failed to extract base assets", e)
|
|
630
|
+
} finally {
|
|
631
|
+
tmp.deleteRecursively()
|
|
624
632
|
}
|
|
625
633
|
}
|
|
626
634
|
return 0
|
|
@@ -998,8 +1006,19 @@ internal class PatchManager(
|
|
|
998
1006
|
meta.put("assetsRef", assetsRef)
|
|
999
1007
|
archiveCurrentPatchToHistory(meta)
|
|
1000
1008
|
} else {
|
|
1001
|
-
// First patch ever
|
|
1002
|
-
|
|
1009
|
+
// First patch ever.
|
|
1010
|
+
//
|
|
1011
|
+
// Only extract the base APK's flutter_assets (as a rollback
|
|
1012
|
+
// reference) when this patch actually ships assets. A Dart-only
|
|
1013
|
+
// (code-only) patch never overlays assets, so extracting the
|
|
1014
|
+
// whole base asset tree here is pure waste — it was the main
|
|
1015
|
+
// cause of a first code-patch ballooning app storage by 100s of
|
|
1016
|
+
// MB. Code-only patches load assets straight from the APK.
|
|
1017
|
+
assetsRef = if (finalAssets != null) {
|
|
1018
|
+
extractBaseAssetsIfNeeded()
|
|
1019
|
+
} else {
|
|
1020
|
+
0
|
|
1021
|
+
}
|
|
1003
1022
|
meta.put("assetsRef", assetsRef)
|
|
1004
1023
|
}
|
|
1005
1024
|
|
|
@@ -169,7 +169,7 @@ const Map<String, Color> _logColors = {
|
|
|
169
169
|
/// - Title bar (traffic lights + status pill)
|
|
170
170
|
/// - META (channel / version / bundle / size)
|
|
171
171
|
/// - STEPS (per-phase status list)
|
|
172
|
-
/// - PROGRESS (
|
|
172
|
+
/// - PROGRESS (single-line determinate bar + percent)
|
|
173
173
|
/// - LOG (scrolling colored feed)
|
|
174
174
|
/// - Footer (deploy message, or error hint + retry)
|
|
175
175
|
class OtaProgressOverlay extends StatefulWidget {
|
|
@@ -381,8 +381,9 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
381
381
|
: '·';
|
|
382
382
|
|
|
383
383
|
// Only the downloading step shows a percentage. Verify / install /
|
|
384
|
-
//
|
|
385
|
-
//
|
|
384
|
+
// Only the download step shows a percentage. Other active phases show
|
|
385
|
+
// a steady "working…" (no spinning glyph in the status column — the
|
|
386
|
+
// row's leading glyph already spins), so nothing jitters.
|
|
386
387
|
final String status;
|
|
387
388
|
if (failed) {
|
|
388
389
|
status = 'error';
|
|
@@ -391,7 +392,7 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
391
392
|
} else if (active) {
|
|
392
393
|
final showPct = s.phase == PatchApplyPhase.downloading &&
|
|
393
394
|
s.fraction != null;
|
|
394
|
-
status = showPct ? _pct(s.fraction) : 'working
|
|
395
|
+
status = showPct ? _pct(s.fraction) : 'working…';
|
|
395
396
|
} else {
|
|
396
397
|
status = 'wait';
|
|
397
398
|
}
|
|
@@ -437,68 +438,53 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
437
438
|
|
|
438
439
|
// ── PROGRESS ──────────────────────────────────────────────────────────
|
|
439
440
|
Widget _progressBlock(OtaOverlayState s) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
441
|
+
// Single honest fraction for the bar:
|
|
442
|
+
// - preparing (no phase / no fraction yet) → empty
|
|
443
|
+
// - downloading with a known size → real fraction
|
|
444
|
+
// - verifying / installing / finalizing / restarting → full (download is
|
|
445
|
+
// already done, so the bar simply stays at 100% — no sliding animation)
|
|
446
|
+
// - error → freeze wherever it was
|
|
447
|
+
final double frac;
|
|
448
|
+
if (s.hasError) {
|
|
449
|
+
frac = (s.fraction ?? 0).clamp(0.0, 1.0);
|
|
450
|
+
} else if (s.restarting) {
|
|
451
|
+
frac = 1.0;
|
|
452
|
+
} else if (s.phase == PatchApplyPhase.downloading) {
|
|
453
|
+
frac = (s.fraction ?? 0).clamp(0.0, 1.0);
|
|
454
|
+
} else if (s.phase == PatchApplyPhase.verifying ||
|
|
455
|
+
s.phase == PatchApplyPhase.finalizing) {
|
|
456
|
+
frac = 1.0;
|
|
457
|
+
} else {
|
|
458
|
+
frac = 0.0; // preparing
|
|
449
459
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
// A determinate fraction is only meaningful while downloading with a known
|
|
455
|
-
// Content-Length. Verifying / finalizing / unknown-size downloads render an
|
|
456
|
-
// indeterminate (sliding) bar so nothing ever looks stuck at 0%.
|
|
457
|
-
final bool determinate = !s.hasError &&
|
|
458
|
-
s.phase == PatchApplyPhase.downloading &&
|
|
459
|
-
s.fraction != null;
|
|
460
|
-
final double? frac = s.hasError
|
|
461
|
-
? (s.fraction ?? 0)
|
|
462
|
-
: (determinate ? s.fraction : null);
|
|
460
|
+
|
|
461
|
+
// Percent read-out only while it's a real download percentage.
|
|
462
|
+
final bool showPct =
|
|
463
|
+
!s.hasError && s.phase == PatchApplyPhase.downloading && s.fraction != null;
|
|
463
464
|
|
|
464
465
|
return _panelBox(
|
|
465
|
-
child:
|
|
466
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
466
|
+
child: Row(
|
|
467
467
|
children: [
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
child: _ProgressBar(
|
|
474
|
-
fraction: frac,
|
|
475
|
-
color: s.hasError ? _C.red : _C.blue,
|
|
476
|
-
),
|
|
477
|
-
),
|
|
478
|
-
const SizedBox(width: 10),
|
|
479
|
-
SizedBox(
|
|
480
|
-
width: 44,
|
|
481
|
-
child: Text(
|
|
482
|
-
determinate ? _pct(s.fraction) : '',
|
|
483
|
-
textAlign: TextAlign.right,
|
|
484
|
-
style: const TextStyle(
|
|
485
|
-
fontFamily: _mono,
|
|
486
|
-
fontSize: 13,
|
|
487
|
-
color: _C.text,
|
|
488
|
-
fontWeight: FontWeight.w700,
|
|
489
|
-
fontFeatures: [FontFeature.tabularFigures()],
|
|
490
|
-
),
|
|
491
|
-
),
|
|
492
|
-
),
|
|
493
|
-
],
|
|
468
|
+
Expanded(
|
|
469
|
+
child: _ProgressBar(
|
|
470
|
+
fraction: frac,
|
|
471
|
+
color: s.hasError ? _C.red : _C.blue,
|
|
472
|
+
),
|
|
494
473
|
),
|
|
495
|
-
const SizedBox(
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
474
|
+
const SizedBox(width: 10),
|
|
475
|
+
SizedBox(
|
|
476
|
+
width: 44,
|
|
477
|
+
child: Text(
|
|
478
|
+
showPct ? _pct(s.fraction) : '',
|
|
479
|
+
textAlign: TextAlign.right,
|
|
480
|
+
style: const TextStyle(
|
|
481
|
+
fontFamily: _mono,
|
|
482
|
+
fontSize: 13,
|
|
483
|
+
color: _C.text,
|
|
484
|
+
fontWeight: FontWeight.w700,
|
|
485
|
+
fontFeatures: [FontFeature.tabularFigures()],
|
|
486
|
+
),
|
|
487
|
+
),
|
|
502
488
|
),
|
|
503
489
|
],
|
|
504
490
|
),
|
|
@@ -605,22 +591,6 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
605
591
|
child: child,
|
|
606
592
|
);
|
|
607
593
|
|
|
608
|
-
Widget _stat(String k, String v) => Expanded(
|
|
609
|
-
child: Column(
|
|
610
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
611
|
-
children: [
|
|
612
|
-
Text(k,
|
|
613
|
-
style: const TextStyle(
|
|
614
|
-
color: _C.faint, fontFamily: _mono, fontSize: 10)),
|
|
615
|
-
const SizedBox(height: 2),
|
|
616
|
-
Text(v,
|
|
617
|
-
overflow: TextOverflow.ellipsis,
|
|
618
|
-
style: const TextStyle(
|
|
619
|
-
color: _C.text, fontFamily: _mono, fontSize: 12)),
|
|
620
|
-
],
|
|
621
|
-
),
|
|
622
|
-
);
|
|
623
|
-
|
|
624
594
|
Widget _dot(Color c) => Container(
|
|
625
595
|
width: 11,
|
|
626
596
|
height: 11,
|
|
@@ -644,19 +614,15 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
|
|
|
644
614
|
);
|
|
645
615
|
}
|
|
646
616
|
|
|
647
|
-
/// A single-line progress bar that never wraps.
|
|
648
|
-
///
|
|
649
|
-
/// - When [fraction] is non-null it's a determinate bar that animates smoothly
|
|
650
|
-
/// between values (no snapping / jumping).
|
|
651
|
-
/// - When [fraction] is null it's an indeterminate sliding bar, so
|
|
652
|
-
/// verify/install/finalize (which have no meaningful percentage) never look
|
|
653
|
-
/// frozen at 0%.
|
|
617
|
+
/// A single-line determinate progress bar that never wraps.
|
|
654
618
|
///
|
|
655
619
|
/// It's a real painted widget with a fixed height inside an [Expanded], so it
|
|
656
620
|
/// always stays on one line regardless of width — unlike the old
|
|
657
|
-
/// character-string bar, which wrapped when it didn't fit.
|
|
621
|
+
/// character-string bar, which wrapped when it didn't fit. It eases toward its
|
|
622
|
+
/// target (forward-only) instead of snapping, and has no indeterminate sliding
|
|
623
|
+
/// mode — once a download completes the bar simply rests at 100%.
|
|
658
624
|
class _ProgressBar extends StatefulWidget {
|
|
659
|
-
final double
|
|
625
|
+
final double fraction;
|
|
660
626
|
final Color color;
|
|
661
627
|
|
|
662
628
|
const _ProgressBar({required this.fraction, required this.color});
|
|
@@ -665,40 +631,22 @@ class _ProgressBar extends StatefulWidget {
|
|
|
665
631
|
State<_ProgressBar> createState() => _ProgressBarState();
|
|
666
632
|
}
|
|
667
633
|
|
|
668
|
-
class _ProgressBarState extends State<_ProgressBar>
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
vsync: this,
|
|
672
|
-
duration: const Duration(milliseconds: 1200),
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
// Smoothed determinate value so the bar eases toward the target instead of
|
|
676
|
-
// snapping. Monotonic — it never visually goes backwards.
|
|
634
|
+
class _ProgressBarState extends State<_ProgressBar> {
|
|
635
|
+
// Smoothed value so the bar eases toward the target. Forward-only — it never
|
|
636
|
+
// visually goes backwards.
|
|
677
637
|
double _shown = 0;
|
|
678
638
|
|
|
679
639
|
@override
|
|
680
640
|
void initState() {
|
|
681
641
|
super.initState();
|
|
682
|
-
_shown = widget.fraction
|
|
683
|
-
if (widget.fraction == null) _indeterminate.repeat();
|
|
642
|
+
_shown = widget.fraction.clamp(0.0, 1.0);
|
|
684
643
|
}
|
|
685
644
|
|
|
686
645
|
@override
|
|
687
646
|
void didUpdateWidget(covariant _ProgressBar old) {
|
|
688
647
|
super.didUpdateWidget(old);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
} else {
|
|
692
|
-
if (_indeterminate.isAnimating) _indeterminate.stop();
|
|
693
|
-
final target = widget.fraction!.clamp(0.0, 1.0);
|
|
694
|
-
if (target >= _shown) _shown = target; // forward-only
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
@override
|
|
699
|
-
void dispose() {
|
|
700
|
-
_indeterminate.dispose();
|
|
701
|
-
super.dispose();
|
|
648
|
+
final target = widget.fraction.clamp(0.0, 1.0);
|
|
649
|
+
if (target >= _shown) _shown = target;
|
|
702
650
|
}
|
|
703
651
|
|
|
704
652
|
static const _height = 8.0;
|
|
@@ -712,60 +660,24 @@ class _ProgressBarState extends State<_ProgressBar>
|
|
|
712
660
|
height: _height,
|
|
713
661
|
child: ColoredBox(
|
|
714
662
|
color: _track,
|
|
715
|
-
child:
|
|
716
|
-
|
|
717
|
-
:
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
tween: Tween(end: _shown),
|
|
729
|
-
duration: const Duration(milliseconds: 400),
|
|
730
|
-
curve: Curves.easeOut,
|
|
731
|
-
builder: (context, v, _) => Align(
|
|
732
|
-
alignment: Alignment.centerLeft,
|
|
733
|
-
child: Container(
|
|
734
|
-
width: (c.maxWidth * v.clamp(0.0, 1.0)),
|
|
735
|
-
decoration: BoxDecoration(
|
|
736
|
-
color: widget.color,
|
|
737
|
-
borderRadius: BorderRadius.circular(99),
|
|
738
|
-
),
|
|
739
|
-
),
|
|
740
|
-
),
|
|
741
|
-
),
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
Widget _indeterminateBar() {
|
|
746
|
-
return LayoutBuilder(
|
|
747
|
-
builder: (context, c) => AnimatedBuilder(
|
|
748
|
-
animation: _indeterminate,
|
|
749
|
-
builder: (context, _) {
|
|
750
|
-
const segment = 0.35; // 35% wide moving segment
|
|
751
|
-
final t = _indeterminate.value; // 0..1
|
|
752
|
-
final left = (t * (1 + segment)) - segment; // slides in from the left
|
|
753
|
-
return Align(
|
|
754
|
-
alignment: Alignment.centerLeft,
|
|
755
|
-
child: Padding(
|
|
756
|
-
padding: EdgeInsets.only(
|
|
757
|
-
left: (left.clamp(0.0, 1.0)) * c.maxWidth,
|
|
758
|
-
),
|
|
759
|
-
child: Container(
|
|
760
|
-
width: segment * c.maxWidth,
|
|
761
|
-
decoration: BoxDecoration(
|
|
762
|
-
color: widget.color,
|
|
763
|
-
borderRadius: BorderRadius.circular(99),
|
|
663
|
+
child: LayoutBuilder(
|
|
664
|
+
builder: (context, c) => TweenAnimationBuilder<double>(
|
|
665
|
+
tween: Tween(end: _shown),
|
|
666
|
+
duration: const Duration(milliseconds: 300),
|
|
667
|
+
curve: Curves.easeOut,
|
|
668
|
+
builder: (context, v, _) => Align(
|
|
669
|
+
alignment: Alignment.centerLeft,
|
|
670
|
+
child: Container(
|
|
671
|
+
width: c.maxWidth * v.clamp(0.0, 1.0),
|
|
672
|
+
decoration: BoxDecoration(
|
|
673
|
+
color: widget.color,
|
|
674
|
+
borderRadius: BorderRadius.circular(99),
|
|
675
|
+
),
|
|
764
676
|
),
|
|
765
677
|
),
|
|
766
678
|
),
|
|
767
|
-
)
|
|
768
|
-
|
|
679
|
+
),
|
|
680
|
+
),
|
|
769
681
|
),
|
|
770
682
|
);
|
|
771
683
|
}
|
|
@@ -869,10 +781,6 @@ class OtaOverlayManager {
|
|
|
869
781
|
bool _disposed = false;
|
|
870
782
|
DateTime? _startTime;
|
|
871
783
|
|
|
872
|
-
// Download-speed tracking.
|
|
873
|
-
int _lastBytes = 0;
|
|
874
|
-
DateTime? _lastBytesAt;
|
|
875
|
-
|
|
876
784
|
/// Last 10%-bucket we emitted a "download NN%" log line for, to throttle the
|
|
877
785
|
/// feed. -1 = none yet.
|
|
878
786
|
int _lastLoggedPctBucket = -1;
|
|
@@ -911,8 +819,6 @@ class OtaOverlayManager {
|
|
|
911
819
|
if (overlay == null) return null;
|
|
912
820
|
|
|
913
821
|
_startTime = DateTime.now();
|
|
914
|
-
_lastBytes = 0;
|
|
915
|
-
_lastBytesAt = null;
|
|
916
822
|
_lastLoggedPctBucket = -1;
|
|
917
823
|
|
|
918
824
|
final logs = <LogLine>[
|
|
@@ -984,30 +890,6 @@ class OtaOverlayManager {
|
|
|
984
890
|
final phaseStep = _phaseToStep(progress.phase);
|
|
985
891
|
final newStep = phaseStep < s.activeStep ? s.activeStep : phaseStep;
|
|
986
892
|
|
|
987
|
-
// Compute a smoothed download speed from byte deltas. A single sample can
|
|
988
|
-
// spike wildly (buffered reads), so we blend with the previous value.
|
|
989
|
-
int? bytesPerSec = s.bytesPerSec;
|
|
990
|
-
if (progress.phase == PatchApplyPhase.downloading &&
|
|
991
|
-
progress.bytesReceived > 0) {
|
|
992
|
-
final now = DateTime.now();
|
|
993
|
-
if (_lastBytesAt != null && progress.bytesReceived > _lastBytes) {
|
|
994
|
-
final dt = now.difference(_lastBytesAt!).inMilliseconds / 1000.0;
|
|
995
|
-
if (dt > 0.05) {
|
|
996
|
-
final sample =
|
|
997
|
-
((progress.bytesReceived - _lastBytes) / dt).round();
|
|
998
|
-
// Exponential moving average → the speed/ETA read-out stops jumping.
|
|
999
|
-
bytesPerSec = bytesPerSec == null || bytesPerSec == 0
|
|
1000
|
-
? sample
|
|
1001
|
-
: (bytesPerSec * 0.6 + sample * 0.4).round();
|
|
1002
|
-
_lastBytes = progress.bytesReceived;
|
|
1003
|
-
_lastBytesAt = now;
|
|
1004
|
-
}
|
|
1005
|
-
} else {
|
|
1006
|
-
_lastBytes = progress.bytesReceived;
|
|
1007
|
-
_lastBytesAt = now;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
893
|
// Log phase transitions once, with staggered detail lines.
|
|
1012
894
|
if (s.phase != progress.phase) {
|
|
1013
895
|
switch (progress.phase) {
|
|
@@ -1048,7 +930,6 @@ class OtaOverlayManager {
|
|
|
1048
930
|
fraction: progress.fraction,
|
|
1049
931
|
bytesReceived: progress.bytesReceived,
|
|
1050
932
|
totalBytes: progress.totalBytes > 0 ? progress.totalBytes : null,
|
|
1051
|
-
bytesPerSec: bytesPerSec,
|
|
1052
933
|
activeStep: newStep,
|
|
1053
934
|
);
|
|
1054
935
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
name: flutter_ota_kit
|
|
2
2
|
description: Open-source, self-hosted code push (OTA updates) for Flutter Android. Patches Dart AOT libapp.so and assets on cold start with integrity checks and crash rollback.
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.2
|
|
4
4
|
homepage: https://github.com/HYPER12755/flutter-ota-kit
|
|
5
5
|
repository: https://github.com/HYPER12755/flutter-ota-kit
|
|
6
6
|
issue_tracker: https://github.com/HYPER12755/flutter-ota-kit/issues
|
package/package.json
CHANGED