diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 15b5a0587d..ddc974ab6e 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -19,13 +19,13 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/pages/common/splash_screen.page.dart'; -import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; -import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/pages/common/splash_screen.page.dart'; +import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; +import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; @@ -120,19 +120,16 @@ class ImmichApp extends ConsumerStatefulWidget { ImmichAppState createState() => ImmichAppState(); } -class ImmichAppState extends ConsumerState with WidgetsBindingObserver { - ProviderSubscription? _authSubscription; - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { +class ImmichAppState extends ConsumerState with WidgetsBindingObserver { + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: - dPrint(() => "[APP STATE] resumed"); - ref.read(appStateProvider.notifier).handleAppResume(); - // Check for ACTION_VIEW intent when app resumes - unawaited(ref.read(viewIntentHandlerProvider).checkForViewIntent()); - unawaited(ref.read(viewIntentHandlerProvider).flushPending()); - break; + dPrint(() => "[APP STATE] resumed"); + ref.read(appStateProvider.notifier).handleAppResume(); + unawaited(ref.read(viewIntentHandlerProvider).onAppResumed()); + break; case AppLifecycleState.inactive: dPrint(() => "[APP STATE] inactive"); ref.read(appStateProvider.notifier).handleAppInactivity(); @@ -215,18 +212,17 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve StaticTranslations.instance.backup_background_service_default_notification, ); } - }); - - ref.read(viewIntentHandlerProvider).init(); - ref.read(shareIntentUploadProvider.notifier).init(); - } - - @override - void dispose() { - _authSubscription?.close(); - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } + }); + + ref.read(viewIntentHandlerProvider).init(); + ref.read(shareIntentUploadProvider.notifier).init(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } @override void reassemble() { diff --git a/mobile/lib/providers/view_intent/view_intent_handler.provider.dart b/mobile/lib/providers/view_intent/view_intent_handler.provider.dart index 81f970d826..e88a163659 100644 --- a/mobile/lib/providers/view_intent/view_intent_handler.provider.dart +++ b/mobile/lib/providers/view_intent/view_intent_handler.provider.dart @@ -1,105 +1,24 @@ -import 'dart:async'; +import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/platform/view_intent_api.g.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/view_intent/view_intent_file_path.provider.dart'; -import 'package:immich_mobile/providers/view_intent/view_intent_pending.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/view_intent.service.dart'; -import 'package:immich_mobile/services/view_intent_asset_resolver.service.dart'; -import 'package:logging/logging.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_handler_android.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_handler_stub.dart'; -final viewIntentHandlerProvider = Provider( - (ref) => ViewIntentHandler( - ref, - ref.read(viewIntentServiceProvider), - ref.read(viewIntentAssetResolverProvider), - ref.watch(appRouterProvider), - ), -); +abstract class ViewIntentHandler { + void init(); -class ViewIntentHandler { - final Ref _ref; - final ViewIntentService _viewIntentService; - final ViewIntentAssetResolver _viewIntentAssetResolver; - final AppRouter _router; - static final Logger _logger = Logger('ViewIntentHandler'); + Future onAppResumed(); - const ViewIntentHandler(this._ref, this._viewIntentService, this._viewIntentAssetResolver, this._router); + Future onUserAuthenticated(); - void init() { - unawaited(checkForViewIntent()); - unawaited( - Future(() async { - await flushPending(); - }), - ); - } - - Future checkForViewIntent() async { - final attachment = await _viewIntentService.consumeViewIntent(); - if (attachment != null) { - await handle(attachment); - return; - } - - if (_ref.read(viewIntentPendingProvider) == null) { - await _viewIntentService.cleanupStaleTempFiles(); - } - } - - Future flushPending() async { - final pendingAttachment = _ref.read(viewIntentPendingProvider.notifier).takeIfFresh(); - _logger.info('flushPending, pendingAttachment:$pendingAttachment}'); - if (pendingAttachment != null) { - await handle(pendingAttachment); - } - } - - Future handle(ViewIntentPayload attachment) async { - _logger.info( - 'handle attachment, mimeType:${attachment.mimeType}, localAssetId=${attachment.localAssetId}, path=${attachment.path}, isAuthenticated:${_ref.read(authProvider).isAuthenticated}', - ); - - if (!_ref.read(authProvider).isAuthenticated) { - _ref.read(viewIntentPendingProvider.notifier).defer(attachment); - return; - } - - final resolvedAsset = await _viewIntentAssetResolver.resolve(attachment); - _logger.fine('resolved view intent asset: ${resolvedAsset.asset}'); - _openAssetViewer( - resolvedAsset.asset, - resolvedAsset.timelineService, - resolvedAsset.initialIndex, - viewIntentFilePath: resolvedAsset.viewIntentFilePath, - ); - } - - void _openAssetViewer( - BaseAsset asset, - TimelineService timelineService, - int initialIndex, { - String? viewIntentFilePath, - }) { - _ref.read(assetViewerProvider.notifier).reset(); - _ref.read(assetViewerProvider.notifier).setAsset(asset); - if (viewIntentFilePath != null) { - _ref.read(viewIntentFilePathProvider.notifier).setPath(viewIntentFilePath); - unawaited(_viewIntentService.setManagedTempFilePath(viewIntentFilePath)); - } else { - _ref.read(viewIntentFilePathProvider.notifier).clear(); - unawaited(_viewIntentService.cleanupManagedTempFile()); - } - - if (asset.isVideo) { - _ref.read(assetViewerProvider.notifier).setControls(true); - } - - _router.push(AssetViewerRoute(initialIndex: initialIndex, timelineService: timelineService)); - } + Future handle(ViewIntentPayload attachment); } + +final viewIntentHandlerProvider = Provider((ref) { + if (Platform.isAndroid) { + return AndroidViewIntentHandler(ref); + } + + return const StubViewIntentHandler(); +}); diff --git a/mobile/lib/providers/view_intent/view_intent_handler_android.dart b/mobile/lib/providers/view_intent/view_intent_handler_android.dart new file mode 100644 index 0000000000..22bb772b20 --- /dev/null +++ b/mobile/lib/providers/view_intent/view_intent_handler_android.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/platform/view_intent_api.g.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_file_path.provider.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_pending.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/view_intent.service.dart'; +import 'package:immich_mobile/services/view_intent_asset_resolver.service.dart'; +import 'package:logging/logging.dart'; + +class AndroidViewIntentHandler implements ViewIntentHandler { + final Ref _ref; + final ViewIntentService _viewIntentService; + final ViewIntentAssetResolver _viewIntentAssetResolver; + final AppRouter _router; + static final Logger _logger = Logger('ViewIntentHandler'); + + AndroidViewIntentHandler(Ref ref) + : _ref = ref, + _viewIntentService = ref.read(viewIntentServiceProvider), + _viewIntentAssetResolver = ref.read(viewIntentAssetResolverProvider), + _router = ref.watch(appRouterProvider); + + @override + void init() { + // Covers cold start from a view intent before the first lifecycle "resumed". + unawaited(onAppResumed()); + } + + @override + Future onAppResumed() => _checkForViewIntent(); + + @override + Future onUserAuthenticated() => _flushPending(); + + Future _checkForViewIntent() async { + final attachment = await _viewIntentService.consumeViewIntent(); + if (attachment != null) { + await handle(attachment); + return; + } + + if (_ref.read(viewIntentPendingProvider) == null) { + await _viewIntentService.cleanupStaleTempFiles(); + } + } + + Future _flushPending() async { + final pendingAttachment = _ref.read(viewIntentPendingProvider.notifier).takeIfFresh(); + _logger.info('flushPending, pendingAttachment:$pendingAttachment}'); + if (pendingAttachment != null) { + await handle(pendingAttachment); + } + } + + @override + Future handle(ViewIntentPayload attachment) async { + _logger.info( + 'handle attachment, mimeType:${attachment.mimeType}, localAssetId=${attachment.localAssetId}, path=${attachment.path}, isAuthenticated:${_ref.read(authProvider).isAuthenticated}', + ); + + if (!_ref.read(authProvider).isAuthenticated) { + _ref.read(viewIntentPendingProvider.notifier).defer(attachment); + return; + } + + final resolvedAsset = await _viewIntentAssetResolver.resolve(attachment); + _logger.fine('resolved view intent asset: ${resolvedAsset.asset}'); + _openAssetViewer( + resolvedAsset.asset, + resolvedAsset.timelineService, + resolvedAsset.initialIndex, + viewIntentFilePath: resolvedAsset.viewIntentFilePath, + ); + } + + void _openAssetViewer( + BaseAsset asset, + TimelineService timelineService, + int initialIndex, { + String? viewIntentFilePath, + }) { + _ref.read(assetViewerProvider.notifier).reset(); + _ref.read(assetViewerProvider.notifier).setAsset(asset); + if (viewIntentFilePath != null) { + _ref.read(viewIntentFilePathProvider.notifier).setPath(viewIntentFilePath); + unawaited(_viewIntentService.setManagedTempFilePath(viewIntentFilePath)); + } else { + _ref.read(viewIntentFilePathProvider.notifier).clear(); + unawaited(_viewIntentService.cleanupManagedTempFile()); + } + + if (asset.isVideo) { + _ref.read(assetViewerProvider.notifier).setControls(true); + } + + _router.push(AssetViewerRoute(initialIndex: initialIndex, timelineService: timelineService)); + } +} diff --git a/mobile/lib/providers/view_intent/view_intent_handler_stub.dart b/mobile/lib/providers/view_intent/view_intent_handler_stub.dart new file mode 100644 index 0000000000..3e642e5f44 --- /dev/null +++ b/mobile/lib/providers/view_intent/view_intent_handler_stub.dart @@ -0,0 +1,18 @@ +import 'package:immich_mobile/platform/view_intent_api.g.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; + +class StubViewIntentHandler implements ViewIntentHandler { + const StubViewIntentHandler(); + + @override + void init() {} + + @override + Future onAppResumed() async {} + + @override + Future onUserAuthenticated() async {} + + @override + Future handle(ViewIntentPayload attachment) async {} +} diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index f464fabbdb..dfbf479b23 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -253,7 +253,7 @@ class LoginForm extends HookConsumerWidget { ref.read(websocketProvider.notifier).connect(); unawaited(context.replaceRoute(const TabShellRoute())); WidgetsBinding.instance.addPostFrameCallback((_) { - unawaited(ref.read(viewIntentHandlerProvider).flushPending()); + unawaited(ref.read(viewIntentHandlerProvider).onUserAuthenticated()); }); return; } @@ -343,7 +343,7 @@ class LoginForm extends HookConsumerWidget { unawaited(handleSyncFlow()); unawaited(context.replaceRoute(const TabShellRoute())); WidgetsBinding.instance.addPostFrameCallback((_) { - unawaited(ref.read(viewIntentHandlerProvider).flushPending()); + unawaited(ref.read(viewIntentHandlerProvider).onUserAuthenticated()); }); return; }