diff --git a/mobile/lib/core/errors.dart b/mobile/lib/core/errors.dart index a7d616b92..d39f6f027 100644 --- a/mobile/lib/core/errors.dart +++ b/mobile/lib/core/errors.dart @@ -79,3 +79,5 @@ class LoginKeyDerivationError extends Error {} class SrpSetupNotCompleteError extends Error {} class SharingNotPermittedForFreeAccountsError extends Error {} + +class NoMediaLocationAccessError extends Error {} diff --git a/mobile/lib/services/remote_sync_service.dart b/mobile/lib/services/remote_sync_service.dart index 45bf36bab..0144d70e6 100644 --- a/mobile/lib/services/remote_sync_service.dart +++ b/mobile/lib/services/remote_sync_service.dart @@ -170,7 +170,8 @@ class RemoteSyncService { e is NoActiveSubscriptionError || e is WiFiUnavailableError || e is StorageLimitExceededError || - e is SyncStopRequestedError) { + e is SyncStopRequestedError || + e is NoMediaLocationAccessError) { _logger.warning("Error executing remote sync", e, s); rethrow; } else { @@ -555,6 +556,7 @@ class RemoteSyncService { final int toBeUploaded = filesToBeUploaded.length + updatedFileIDs.length; if (toBeUploaded > 0) { Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparingForUpload)); + await _uploader.verifyMediaLocationAccess(); await _uploader.checkNetworkForUpload(); // verify if files upload is allowed based on their subscription plan and // storage limit. To avoid creating new endpoint, we are using diff --git a/mobile/lib/services/sync_service.dart b/mobile/lib/services/sync_service.dart index 8de3e3322..057e600df 100644 --- a/mobile/lib/services/sync_service.dart +++ b/mobile/lib/services/sync_service.dart @@ -120,6 +120,14 @@ class SyncService { } on UnauthorizedError { _logger.info("Logging user out"); Bus.instance.fire(TriggerLogoutEvent()); + } on NoMediaLocationAccessError { + _logger.severe("Not uploading due to no media location access"); + Bus.instance.fire( + SyncStatusUpdate( + SyncStatus.error, + error: NoMediaLocationAccessError(), + ), + ); } catch (e) { if (e is DioError) { if (e.type == DioErrorType.connectTimeout || diff --git a/mobile/lib/ui/viewer/file/zoomable_image.dart b/mobile/lib/ui/viewer/file/zoomable_image.dart index 933ff748a..90a4fae25 100644 --- a/mobile/lib/ui/viewer/file/zoomable_image.dart +++ b/mobile/lib/ui/viewer/file/zoomable_image.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import "package:flutter_image_compress/flutter_image_compress.dart"; import 'package:logging/logging.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photos/core/cache/thumbnail_in_memory_cache.dart'; @@ -50,6 +51,7 @@ class _ZoomableImageState extends State { bool _loadedLargeThumbnail = false; bool _loadingFinalImage = false; bool _loadedFinalImage = false; + bool _convertToSupportedFormat = false; ValueChanged? _scaleStateChangedCallback; bool _isZooming = false; PhotoViewController _photoViewController = PhotoViewController(); @@ -194,11 +196,8 @@ class _ZoomableImageState extends State { _loadingFinalImage = true; getFileFromServer(_photo).then((file) { if (file != null) { - _onFinalImageLoaded( - Image.file( - file, - gaplessPlayback: true, - ).image, + _onFileLoaded( + file, ); } else { _loadingFinalImage = false; @@ -239,7 +238,9 @@ class _ZoomableImageState extends State { _isGIF(), // since on iOS GIFs playback only when origin-files are loaded ).then((file) { if (file != null && file.existsSync()) { - _onFinalImageLoaded(Image.file(file).image); + _onFileLoaded( + file, + ); } else { _logger.info("File was deleted " + _photo.toString()); if (_photo.uploadedFileID != null) { @@ -277,24 +278,45 @@ class _ZoomableImageState extends State { } } - void _onFinalImageLoaded(ImageProvider imageProvider) { + void _onFileLoaded(File file) { + final imageProvider = Image.file( + file, + gaplessPlayback: true, + ).image; + if (mounted) { - precacheImage(imageProvider, context).then((value) async { - if (mounted) { - await _updatePhotoViewController( - previewImageProvider: _imageProvider, - finalImageProvider: imageProvider, - ); - setState(() { - _imageProvider = imageProvider; - _loadedFinalImage = true; - _logger.info("Final image loaded"); - }); + precacheImage( + imageProvider, + context, + onError: (exception, _) async { + _logger + .info(exception.toString() + ". Filename: ${_photo.displayName}"); + if (exception.toString().contains( + "Codec failed to produce an image, possibly due to invalid image data", + )) { + unawaited(_loadInSupportedFormat(file)); + } + }, + ).then((value) { + if (mounted && !_loadedFinalImage && !_convertToSupportedFormat) { + _updateViewWithFinalImage(imageProvider); } }); } } + Future _updateViewWithFinalImage(ImageProvider imageProvider) async { + await _updatePhotoViewController( + previewImageProvider: _imageProvider, + finalImageProvider: imageProvider, + ); + setState(() { + _imageProvider = imageProvider; + _loadedFinalImage = true; + _logger.info("Final image loaded"); + }); + } + Future _updatePhotoViewController({ required ImageProvider? previewImageProvider, required ImageProvider finalImageProvider, @@ -348,4 +370,28 @@ class _ZoomableImageState extends State { } bool _isGIF() => _photo.displayName.toLowerCase().endsWith(".gif"); + + Future _loadInSupportedFormat(File file) async { + _logger.info("Compressing ${_photo.displayName} to viewable format"); + _convertToSupportedFormat = true; + + final compressedFile = + await FlutterImageCompress.compressWithFile(file.path); + + if (compressedFile != null) { + final imageProvider = MemoryImage(compressedFile); + + unawaited( + precacheImage(imageProvider, context).then((value) { + if (mounted) { + _updateViewWithFinalImage(imageProvider); + } + }), + ); + } else { + _logger.severe( + "Failed to compress image ${_photo.displayName} to viewable format", + ); + } + } } diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index 89a12f147..7c898f985 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -9,6 +9,7 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; +import "package:permission_handler/permission_handler.dart"; import 'package:photos/core/configuration.dart'; import "package:photos/core/constants.dart"; import 'package:photos/core/errors.dart'; @@ -363,6 +364,15 @@ class FileUploader { } } + Future verifyMediaLocationAccess() async { + if (Platform.isAndroid) { + final bool hasPermission = await Permission.accessMediaLocation.isGranted; + if (!hasPermission) { + throw NoMediaLocationAccessError(); + } + } + } + Future forceUpload(EnteFile file, int collectionID) async { _hasInitiatedForceUpload = true; return _tryToUpload(file, collectionID, true); diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 39229bfb3..a4d34d05a 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1538,6 +1538,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8" + url: "https://pub.dev" + source: hosted + version: "11.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e + url: "https://pub.dev" + source: hosted + version: "11.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + url: "https://pub.dev" + source: hosted + version: "9.1.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + url: "https://pub.dev" + source: hosted + version: "0.1.3" petitparser: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index a5659d4ba..5b8cad3f7 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -130,6 +130,7 @@ dependencies: path: #dart path_provider: ^2.1.1 pedantic: ^1.9.2 + permission_handler: ^11.0.1 photo_manager: ^2.8.1 photo_view: ^0.14.0 pinput: ^1.2.2