@_nazmiforreal/flutter-ota 0.1.28 → 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.
Files changed (27) hide show
  1. package/bin/flutter-ota-linux-x64 +0 -0
  2. package/bin/flutter-ota.js +0 -0
  3. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
  4. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +754 -0
  5. package/dart-src/packages/cli-tools/.dart_tool/version +1 -0
  6. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +22 -1
  7. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +17 -8
  8. package/dart-src/packages/cli-tools/pubspec.lock +636 -0
  9. package/dart-src/packages/cli-tools/pubspec.yaml +7 -7
  10. package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +2 -1
  11. package/dart-src/root/analysis_options.yaml +3 -0
  12. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherPlugin.kt +1 -0
  13. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatchManager.kt +24 -5
  14. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfig.kt +20 -0
  15. package/dart-src/root/lib/flutter_ota_kit.dart +31 -11
  16. package/dart-src/root/lib/src/aws_update_source.dart +1 -0
  17. package/dart-src/root/lib/src/cloudflare_update_source.dart +1 -0
  18. package/dart-src/root/lib/src/ota_progress_overlay.dart +243 -296
  19. package/dart-src/root/lib/src/patcher_channel.dart +14 -1
  20. package/dart-src/root/lib/src/pkg/plugin_core/src/request_update_bundle_state.dart +2 -1
  21. package/dart-src/root/lib/src/pkg/postgres/src/postgres_types.dart +2 -1
  22. package/dart-src/root/lib/src/pocketbase_update_source.dart +1 -0
  23. package/dart-src/root/lib/src/postgres_update_source.dart +1 -0
  24. package/dart-src/root/lib/src/shared_update_check.dart +30 -2
  25. package/dart-src/root/lib/src/supabase_update_source.dart +1 -0
  26. package/dart-src/root/pubspec.yaml +14 -14
  27. package/package.json +1 -1
@@ -613,14 +613,22 @@ internal class PatchManager(
613
613
  }
614
614
 
615
615
  private fun extractBaseAssetsIfNeeded(): Int {
616
- // Extract base APK assets to assets/0/ if not already done
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(File(assetsHistoryDir, "0"))
621
- writeFlutterAssetsArchive(File(assetsHistoryDir, "0"), baseAssetsArchive)
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 - extract base assets
1002
- assetsRef = extractBaseAssetsIfNeeded()
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
 
@@ -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')) return RollbackOutcome.fallbackToBase;
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
- patchHash.isNotEmpty ? patchHash.substring(0, patchHash.length.clamp(0, 8)) : null;
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('applyUpdate: patch failed (${error.name}), blacklisting $patchVersion');
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('applyUpdate: patch failed (${error?.name}); transient, not '
893
- 'blacklisting (will retry next check)');
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
- // Wait for overlay to finish its minimum display time + success dwell
900
- await Future.delayed(const Duration(seconds: 3));
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 ? 'previous' : 'base',
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(patchVersion, md5: patchMd5)) {
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(String version, {String md5 = ''}) async {
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);
@@ -91,6 +91,7 @@ class AwsUpdateSource {
91
91
  fingerprintHash: config.fingerprintHash,
92
92
  minBundleId: config.minBundleId,
93
93
  currentBundleId: currentBundleId,
94
+ cohort: config.cohort,
94
95
  timeout: timeout,
95
96
  );
96
97
  }
@@ -91,6 +91,7 @@ class CloudflareUpdateSource {
91
91
  fingerprintHash: config.fingerprintHash,
92
92
  minBundleId: config.minBundleId,
93
93
  currentBundleId: currentBundleId,
94
+ cohort: config.cohort,
94
95
  timeout: timeout,
95
96
  );
96
97
  }