From 2d2673c11490b75c7a69b6d3cec396a21e264644 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 10 Sep 2025 16:27:00 -0500 Subject: [PATCH] fix: repeat timeline migration for first time user (#21794) --- .../pages/common/change_experience.page.dart | 98 +++++++++++-------- .../providers/app_life_cycle.provider.dart | 7 -- mobile/lib/utils/migration.dart | 6 +- .../sync_status_and_actions.dart | 4 +- 4 files changed, 63 insertions(+), 52 deletions(-) diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart index 8779eecd7f..47e18470ca 100644 --- a/mobile/lib/pages/common/change_experience.page.dart +++ b/mobile/lib/pages/common/change_experience.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -42,49 +44,13 @@ class _ChangeExperiencePageState extends ConsumerState { Future _handleMigration() async { try { - if (widget.switchingToBeta) { - final assetNotifier = ref.read(assetProvider.notifier); - if (assetNotifier.mounted) { - assetNotifier.dispose(); - } - final albumNotifier = ref.read(albumProvider.notifier); - if (albumNotifier.mounted) { - albumNotifier.dispose(); - } - - // Cancel uploads - await Store.put(StoreKey.backgroundBackup, false); - ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); - ref.read(backupProvider.notifier).setAutoBackup(false); - ref.read(backupProvider.notifier).cancelBackup(); - ref.read(manualUploadProvider.notifier).cancelBackup(); - // Start listening to new websocket events - ref.read(websocketProvider.notifier).stopListenToOldEvents(); - ref.read(websocketProvider.notifier).startListeningToBetaEvents(); - - final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - - if (permission.isGranted) { - await ref.read(backgroundSyncProvider).syncLocal(full: true); - await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).disableService(); - } - } else { - await ref.read(backgroundSyncProvider).cancel(); - ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); - ref.read(websocketProvider.notifier).startListeningToOldEvents(); - ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); - await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - await ref.read(backgroundWorkerFgServiceProvider).disable(); - } - - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + await _performMigrationLogic().timeout( + const Duration(minutes: 3), + onTimeout: () async { + await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + }, + ); if (mounted) { setState(() { @@ -102,6 +68,52 @@ class _ChangeExperiencePageState extends ConsumerState { } } + Future _performMigrationLogic() async { + if (widget.switchingToBeta) { + final assetNotifier = ref.read(assetProvider.notifier); + if (assetNotifier.mounted) { + assetNotifier.dispose(); + } + final albumNotifier = ref.read(albumProvider.notifier); + if (albumNotifier.mounted) { + albumNotifier.dispose(); + } + + // Cancel uploads + await Store.put(StoreKey.backgroundBackup, false); + ref + .read(backupProvider.notifier) + .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); + ref.read(backupProvider.notifier).setAutoBackup(false); + ref.read(backupProvider.notifier).cancelBackup(); + ref.read(manualUploadProvider.notifier).cancelBackup(); + // Start listening to new websocket events + ref.read(websocketProvider.notifier).stopListenToOldEvents(); + ref.read(websocketProvider.notifier).startListeningToBetaEvents(); + + final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + + if (permission.isGranted) { + await ref.read(backgroundSyncProvider).syncLocal(full: true); + await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); + await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); + await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider)); + await ref.read(backgroundServiceProvider).disableService(); + } + } else { + await ref.read(backgroundSyncProvider).cancel(); + ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); + ref.read(websocketProvider.notifier).startListeningToOldEvents(); + ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); + await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); + await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + await ref.read(backgroundWorkerFgServiceProvider).disable(); + } + + await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index eb6def9353..e5a26272b0 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -167,13 +167,6 @@ class AppLifeCycleNotifier extends StateNotifier { } } catch (e, stackTrace) { _log.severe("Error during background sync", e, stackTrace); - } finally { - // Ensure lock is released even if operations fail - try { - _log.info("Lock released after background sync operations"); - } catch (lockError) { - _log.warning("Failed to release lock after error: $lockError"); - } } } diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index c5fb5edc0f..e5182a5999 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -63,7 +63,10 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { // Handle migration only for this version // TODO: remove when old timeline is removed - if (version == 15) { + final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration); + if (version == 15 && needBetaMigration == null) { + // Check both databases directly instead of relying on cache + final isBeta = Store.tryGet(StoreKey.betaTimeline); final isNewInstallation = await _isNewInstallation(db, drift); @@ -71,6 +74,7 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { // For existing installations, only migrate if beta timeline is not enabled (null or false) if (isNewInstallation || isBeta == true) { await Store.put(StoreKey.needBetaMigration, false); + await Store.put(StoreKey.betaTimeline, true); } else { await resetDriftDatabase(drift); await Store.put(StoreKey.needBetaMigration, true); diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index e32df03cab..15c015736b 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -104,7 +104,6 @@ class SyncStatusAndActions extends HookConsumerWidget { padding: const EdgeInsets.only(top: 16, bottom: 32), child: ListView( children: [ - _SectionHeaderText(text: "assets".t(context: context)), const _SyncStatsCounts(), const Divider(height: 1, indent: 16, endIndent: 16), const SizedBox(height: 24), @@ -270,7 +269,10 @@ class _SyncStatsCounts extends ConsumerWidget { final localHashedCount = snapshot.data![4]! as int; return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ + _SectionHeaderText(text: "assets".t(context: context)), Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Flex(