Jelajahi Sumber

Merge pull request #18 from ente-io/share_to_ente_android

[Android] Share to ente app
Neeraj Gupta 4 tahun lalu
induk
melakukan
726f03439c

+ 14 - 0
android/app/src/main/AndroidManifest.xml

@@ -18,6 +18,20 @@
                 <category android:name="android.intent.category.BROWSABLE" />
                 <data android:scheme="ente"/>
             </intent-filter>
+
+            <!--Filter to support sharing images into our app-->
+            <intent-filter android:label="backup">
+                <action android:name="android.intent.action.SEND" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="image/*" />
+            </intent-filter>
+
+            <intent-filter android:label="backup">
+                <action android:name="android.intent.action.SEND_MULTIPLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="image/*" />
+            </intent-filter>
+
         </activity>
         <!-- Don't delete the meta-data below.
              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

+ 9 - 2
lib/core/configuration.dart

@@ -66,6 +66,7 @@ class Configuration {
   FlutterSecureStorage _secureStorage;
   String _tempDirectory;
   String _thumbnailCacheDirectory;
+  String _sharedMediaDirectory;
   String _volatilePassword;
 
   final _secureStorageOptionsIOS =
@@ -92,9 +93,11 @@ class Configuration {
       _logger.warning(e);
     }
     tempDirectory.createSync(recursive: true);
-    _thumbnailCacheDirectory =
-        (await getTemporaryDirectory()).path + "/thumbnail-cache";
+    var tempDirectoryPath = (await getTemporaryDirectory()).path;
+    _thumbnailCacheDirectory = tempDirectoryPath + "/thumbnail-cache";
     io.Directory(_thumbnailCacheDirectory).createSync(recursive: true);
+    _sharedMediaDirectory = tempDirectoryPath + "/ente-shared-media";
+    io.Directory(_sharedMediaDirectory).createSync(recursive: true);
     if (!_preferences.containsKey(tokenKey)) {
       await _secureStorage.deleteAll(iOptions: _secureStorageOptionsIOS);
     } else {
@@ -412,6 +415,10 @@ class Configuration {
     return _thumbnailCacheDirectory;
   }
 
+  String getSharedMediaCacheDirectory() {
+    return _sharedMediaDirectory;
+  }
+
   bool hasConfiguredAccount() {
     return getToken() != null && _key != null;
   }

+ 3 - 0
lib/core/constants.dart

@@ -11,3 +11,6 @@ const String kRoadmapURL = "https://roadmap.ente.io";
 const int kMicroSecondsInDay = 86400000000;
 const int kAndroid11SDKINT = 30;
 const int kGalleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
+
+// used to identify which ente file are available in app cache
+const String kSharedMediaIdentifier = 'ente-shared://';

+ 15 - 6
lib/db/files_db.dart

@@ -639,13 +639,22 @@ class FilesDB {
     );
   }
 
-  Future<int> deleteLocalFile(String localID) async {
+  Future<int> deleteLocalFile(File file) async {
     final db = await instance.database;
-    return db.delete(
-      table,
-      where: '$columnLocalID =?',
-      whereArgs: [localID],
-    );
+    if (file.localID != null) {
+      // delete all files with same local ID
+      return db.delete(
+        table,
+        where: '$columnLocalID =?',
+        whereArgs: [file.localID],
+      );
+    } else {
+      return db.delete(
+        table,
+        where: '$columnGeneratedID =?',
+        whereArgs: [file.generatedID],
+      );
+    }
   }
 
   Future<void> deleteLocalFiles(List<String> localIDs) async {

+ 11 - 1
lib/models/file.dart

@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
 import 'package:path/path.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/configuration.dart';
+import 'package:photos/core/constants.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/models/location.dart';
 
@@ -61,6 +62,9 @@ class File {
   }
 
   Future<AssetEntity> getAsset() {
+    if (localID == null) {
+      return Future.value(null);
+    }
     return AssetEntity.fromId(localID);
   }
 
@@ -82,7 +86,7 @@ class File {
 
   Map<String, dynamic> getMetadata() {
     final metadata = <String, dynamic>{};
-    metadata["localID"] = localID;
+    metadata["localID"] = isSharedMediaToAppSandbox() ? null : localID;
     metadata["title"] = title;
     metadata["deviceFolder"] = deviceFolder;
     metadata["creationTime"] = creationTime;
@@ -129,10 +133,16 @@ class File {
     }
   }
 
+  // returns true if the file isn't available in the user's gallery
   bool isRemoteFile() {
     return localID == null && uploadedFileID != null;
   }
 
+
+  bool isSharedMediaToAppSandbox() {
+    return localID != null && localID.startsWith(kSharedMediaIdentifier);
+  }
+
   bool hasLocation() {
     return location != null &&
         (location.longitude != 0 || location.latitude != 0);

+ 23 - 12
lib/ui/create_collection_page.dart

@@ -15,11 +15,14 @@ import 'package:photos/ui/loading_widget.dart';
 import 'package:photos/ui/thumbnail_widget.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/file_uploader.dart';
+import 'package:photos/utils/share_util.dart';
 import 'package:photos/utils/toast_util.dart';
+import 'package:receive_sharing_intent/receive_sharing_intent.dart';
 
 class CreateCollectionPage extends StatefulWidget {
   final SelectedFiles selectedFiles;
-  const CreateCollectionPage(this.selectedFiles, {Key key}) : super(key: key);
+  final List<SharedMediaFile> sharedFiles;
+  const CreateCollectionPage(this.selectedFiles,this.sharedFiles, {Key key}) : super(key: key);
 
   @override
   _CreateCollectionPageState createState() => _CreateCollectionPageState();
@@ -220,21 +223,29 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
     final dialog = createProgressDialog(context, "uploading files to album...");
     await dialog.show();
     try {
-      final files = List<File>();
-      for (final file in widget.selectedFiles.files) {
-        final currentFile = await FilesDB.instance.getFile(file.generatedID);
-        if (currentFile.uploadedFileID == null) {
-          final uploadedFile = (await FileUploader.instance
-              .forceUpload(currentFile, collectionID));
-          files.add(uploadedFile);
-        } else {
-          files.add(currentFile);
+      final List<File> files = [];
+      if (widget.sharedFiles != null) {
+        var localFiles = await convertIncomingSharedMediaToFile(
+            widget.sharedFiles,
+          collectionID);
+        FilesDB.instance.insertMultiple(localFiles);
+      } else {
+        for (final file in widget.selectedFiles.files) {
+          await FilesDB.instance.insert(file);
+          final currentFile = await FilesDB.instance.getFile(file.generatedID);
+          if (currentFile.uploadedFileID == null) {
+            final uploadedFile = (await FileUploader.instance
+                .forceUpload(currentFile, collectionID));
+            files.add(uploadedFile);
+          } else {
+            files.add(currentFile);
+          }
         }
+        await CollectionsService.instance.addToCollection(collectionID, files);
       }
-      await CollectionsService.instance.addToCollection(collectionID, files);
       RemoteSyncService.instance.sync(silently: true);
       await dialog.hide();
-      widget.selectedFiles.clearAll();
+      widget.selectedFiles?.clearAll();
       return true;
     } catch (e, s) {
       _logger.severe("Could not add to album", e, s);

+ 1 - 0
lib/ui/gallery_app_bar_widget.dart

@@ -156,6 +156,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
             type: PageTransitionType.bottomToTop,
             child: CreateCollectionPage(
               widget.selectedFiles,
+              null,
             )));
   }
 

+ 29 - 0
lib/ui/home_widget.dart

@@ -27,6 +27,7 @@ import 'package:photos/ui/app_update_dialog.dart';
 import 'package:photos/ui/backup_folder_selection_page.dart';
 import 'package:photos/ui/collections_gallery_widget.dart';
 import 'package:photos/ui/common_elements.dart';
+import 'package:photos/ui/create_collection_page.dart';
 import 'package:photos/ui/extents_page_view.dart';
 import 'package:photos/ui/gallery.dart';
 import 'package:photos/ui/gallery_app_bar_widget.dart';
@@ -41,6 +42,7 @@ import 'package:photos/ui/shared_collections_gallery.dart';
 import 'package:photos/ui/sync_indicator.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/navigation_util.dart';
+import 'package:receive_sharing_intent/receive_sharing_intent.dart';
 import 'package:uni_links/uni_links.dart';
 
 class HomeWidget extends StatefulWidget {
@@ -62,6 +64,10 @@ class _HomeWidgetState extends State<HomeWidget> {
   int _selectedTabIndex = 0;
   Widget _headerWidgetWithSettingsButton;
 
+  // for receiving media files
+  StreamSubscription _intentDataStreamSubscription;
+  List<SharedMediaFile> _sharedFiles;
+
   StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
   StreamSubscription<SubscriptionPurchasedEvent> _subscriptionPurchaseEvent;
   StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
@@ -169,9 +175,28 @@ class _HomeWidgetState extends State<HomeWidget> {
         });
       }
     });
+    // For sharing images coming from outside the app while the app is in the memory
+    _initMediaShareSubscription();
     super.initState();
   }
 
+  void _initMediaShareSubscription() {
+    _intentDataStreamSubscription =
+        ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
+          setState(() {
+            _sharedFiles = value;
+          });
+        }, onError: (err) {
+          _logger.severe("getIntentDataStream error: $err");
+        });
+    // For sharing images coming from outside the app while the app is closed
+    ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
+      setState(() {
+        _sharedFiles = value;
+      });
+    });
+  }
+
   @override
   Widget build(BuildContext context) {
     _logger.info("Building home_Widget");
@@ -211,6 +236,10 @@ class _HomeWidgetState extends State<HomeWidget> {
     if (!LocalSyncService.instance.hasCompletedFirstImport()) {
       return LoadingPhotosWidget();
     }
+    if (_sharedFiles != null && _sharedFiles.isNotEmpty) {
+      ReceiveSharingIntent.reset();
+      return CreateCollectionPage(null, _sharedFiles);
+    }
 
     return Stack(
       children: [

+ 5 - 3
lib/ui/image_editor_page.dart

@@ -320,9 +320,11 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
       newFile.collectionID = widget.originalFile.collectionID;
       newFile.location = widget.originalFile.location;
       if (!newFile.hasLocation() && widget.originalFile.localID != null) {
-        final latLong =
-            await (await widget.originalFile.getAsset()).latlngAsync();
-        newFile.location = Location(latLong.latitude, latLong.longitude);
+        var assetEntity = await widget.originalFile.getAsset();
+        if (assetEntity != null) {
+          final latLong = await assetEntity.latlngAsync();
+          newFile.location = Location(latLong.latitude, latLong.longitude);
+        }
       }
       newFile.generatedID = await FilesDB.instance.insert(newFile);
       await LocalSyncService.instance.trackEditedFile(newFile);

+ 11 - 16
lib/ui/thumbnail_widget.dart

@@ -171,31 +171,26 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
   }
 
   Future _getThumbnailFromDisk() async {
-    widget.file.getAsset().then((asset) async {
-      if (asset == null || !(await asset.exists)) {
+    getThumbnailFromLocal(widget.file).then((thumbData) async {
+      if (thumbData == null) {
         if (widget.file.uploadedFileID != null) {
+          _logger.fine("Removing localID reference for " + widget.file.tag());
           widget.file.localID = null;
           FilesDB.instance.update(widget.file);
           _loadNetworkImage();
         } else {
-          FilesDB.instance.deleteLocalFile(widget.file.localID);
+          _logger.info("Deleting file " + widget.file.tag());
+          FilesDB.instance.deleteLocalFile(widget.file);
           Bus.instance.fire(LocalPhotosUpdatedEvent([widget.file]));
         }
         return;
       }
-      asset
-          .thumbDataWithSize(
-        kThumbnailSmallSize,
-        kThumbnailSmallSize,
-        quality: kThumbnailQuality,
-      )
-          .then((data) {
-        if (data != null && mounted) {
-          final imageProvider = Image.memory(data).image;
-          _cacheAndRender(imageProvider);
-        }
-        ThumbnailLruCache.put(widget.file, data, kThumbnailSmallSize);
-      });
+
+      if (thumbData != null && mounted) {
+        final imageProvider = Image.memory(thumbData).image;
+        _cacheAndRender(imageProvider);
+      }
+      ThumbnailLruCache.put(widget.file, thumbData, kThumbnailSmallSize);
     }).catchError((e) {
       _logger.warning("Could not load image: ", e);
       _encounteredErrorLoadingThumbnail = true;

+ 20 - 46
lib/ui/zoomable_image.dart

@@ -130,57 +130,31 @@ class _ZoomableImageState extends State<ZoomableImage>
         !_loadedLargeThumbnail &&
         !_loadedFinalImage) {
       _loadingLargeThumbnail = true;
-      final cachedThumbnail =
-          ThumbnailLruCache.get(_photo, kThumbnailLargeSize);
-      if (cachedThumbnail != null) {
-        _onLargeThumbnailLoaded(Image.memory(cachedThumbnail).image, context);
-      } else {
-        _photo.getAsset().then((asset) {
-          if (asset == null) {
-            // Deleted file
-            return;
-          }
-          asset
-              .thumbDataWithSize(kThumbnailLargeSize, kThumbnailLargeSize)
-              .then((data) {
-            if (data == null) {
-              // Deleted file
-              return;
-            }
-            _onLargeThumbnailLoaded(Image.memory(data).image, context);
-            ThumbnailLruCache.put(_photo, data, kThumbnailLargeSize);
-          });
-        });
-      }
+      getThumbnailFromLocal(_photo, size: kThumbnailLargeSize, quality: 100)
+          .then((cachedThumbnail) {
+        if(cachedThumbnail != null) {
+          _onLargeThumbnailLoaded(Image.memory(cachedThumbnail).image, context);
+        }
+      });
     }
 
     if (!_loadingFinalImage && !_loadedFinalImage) {
       _loadingFinalImage = true;
-      final cachedFile = FileLruCache.get(_photo);
-      if (cachedFile != null && cachedFile.existsSync()) {
-        _onFinalImageLoaded(Image.file(cachedFile).image);
-      } else {
-        _photo.getAsset().then((asset) async {
-          if (asset == null || !(await asset.exists)) {
-            _logger.info("File was deleted " + _photo.toString());
-            if (_photo.uploadedFileID != null) {
-              _photo.localID = null;
-              FilesDB.instance.update(_photo);
-              _loadNetworkImage();
-            } else {
-              FilesDB.instance.deleteLocalFile(_photo.localID);
-              Bus.instance.fire(LocalPhotosUpdatedEvent([_photo]));
-            }
-            return;
+      getFile(_photo).then((file) {
+        if (file != null && file.existsSync()) {
+          _onFinalImageLoaded(Image.file(file).image);
+        } else {
+          _logger.info("File was deleted " + _photo.toString());
+          if (_photo.uploadedFileID != null) {
+            _photo.localID = null;
+            FilesDB.instance.update(_photo);
+            _loadNetworkImage();
+          } else {
+            FilesDB.instance.deleteLocalFile(_photo);
+            Bus.instance.fire(LocalPhotosUpdatedEvent([_photo]));
           }
-          asset.file.then((file) {
-            if (mounted) {
-              _onFinalImageLoaded(Image.file(file).image);
-              FileLruCache.put(_photo, file);
-            }
-          });
-        });
-      }
+        }
+      });
     }
   }
 

+ 75 - 15
lib/utils/delete_file_util.dart

@@ -1,4 +1,5 @@
 import 'dart:async';
+import 'dart:io' as io;
 import 'dart:io';
 import 'dart:math';
 
@@ -7,6 +8,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 import 'package:logging/logging.dart';
 import 'package:photo_manager/photo_manager.dart';
+import 'package:photos/core/configuration.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/db/files_db.dart';
@@ -20,6 +22,8 @@ import 'package:photos/ui/linear_progress_dialog.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/toast_util.dart';
 
+import 'file_util.dart';
+
 final _logger = Logger("DeleteFileUtil");
 
 Future<void> deleteFilesFromEverywhere(
@@ -27,25 +31,28 @@ Future<void> deleteFilesFromEverywhere(
   final dialog = createProgressDialog(context, "deleting...");
   await dialog.show();
   _logger.info("Trying to delete files " + files.toString());
-  final List<String> localIDs = [];
+  final List<String> localAssetIDs = [];
+  final List<String> localSharedMediaIDs = [];
   final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
   for (final file in files) {
-    if (file.localID != null) {
-      final asset = await file.getAsset();
-      if (asset == null || !(await asset.exists)) {
+    if (file.localID != null)  {
+      if (!(await _localFileExist(file))) {
         _logger.warning("Already deleted " + file.toString());
         alreadyDeletedIDs.add(file.localID);
+      } else if(file.isSharedMediaToAppSandbox()) {
+        localSharedMediaIDs.add(file.localID);
       } else {
-        localIDs.add(file.localID);
+        localAssetIDs.add(file.localID);
       }
     }
   }
   Set<String> deletedIDs = Set<String>();
   try {
-    deletedIDs = (await PhotoManager.editor.deleteWithIds(localIDs)).toSet();
+    deletedIDs = (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
   } catch (e, s) {
     _logger.severe("Could not delete file", e, s);
   }
+  deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
   final updatedCollectionIDs = Set<int>();
   final List<int> uploadedFileIDsToBeDeleted = [];
   final List<File> deletedFiles = [];
@@ -59,7 +66,7 @@ Future<void> deleteFilesFromEverywhere(
           uploadedFileIDsToBeDeleted.add(file.uploadedFileID);
           updatedCollectionIDs.add(file.collectionID);
         } else {
-          await FilesDB.instance.deleteLocalFile(file.localID);
+          await FilesDB.instance.deleteLocalFile(file);
         }
       }
     } else {
@@ -106,25 +113,28 @@ Future<void> deleteFilesOnDeviceOnly(
   final dialog = createProgressDialog(context, "deleting...");
   await dialog.show();
   _logger.info("Trying to delete files " + files.toString());
-  final List<String> localIDs = [];
+  final List<String> localAssetIDs = [];
+  final List<String> localSharedMediaIDs = [];
   final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
   for (final file in files) {
     if (file.localID != null) {
-      final asset = await file.getAsset();
-      if (asset == null || !(await asset.exists)) {
+      if (!(await _localFileExist(file))) {
         _logger.warning("Already deleted " + file.toString());
         alreadyDeletedIDs.add(file.localID);
+      } else if(file.isSharedMediaToAppSandbox()) {
+        localSharedMediaIDs.add(file.localID);
       } else {
-        localIDs.add(file.localID);
+        localAssetIDs.add(file.localID);
       }
     }
   }
   Set<String> deletedIDs = Set<String>();
   try {
-    deletedIDs = (await PhotoManager.editor.deleteWithIds(localIDs)).toSet();
+    deletedIDs = (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
   } catch (e, s) {
     _logger.severe("Could not delete file", e, s);
   }
+  deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
   final List<File> deletedFiles = [];
   for (final file in files) {
     // Remove only those files that have been removed from disk
@@ -145,15 +155,26 @@ Future<void> deleteFilesOnDeviceOnly(
 Future<bool> deleteLocalFiles(
     BuildContext context, List<String> localIDs) async {
   final List<String> deletedIDs = [];
+  final List<String> localAssetIDs = [];
+  final List<String> localSharedMediaIDs = [];
+  for (String id in localIDs) {
+    if (id.startsWith(kSharedMediaIdentifier)) {
+      localSharedMediaIDs.add(id);
+    } else {
+      localAssetIDs.add(id);
+    }
+  }
+  deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
+
   if (Platform.isAndroid) {
     final androidInfo = await DeviceInfoPlugin().androidInfo;
     if (androidInfo.version.sdkInt < kAndroid11SDKINT) {
-      deletedIDs.addAll(await _deleteLocalFilesInBatches(context, localIDs));
+      deletedIDs.addAll(await _deleteLocalFilesInBatches(context, localAssetIDs));
     } else {
-      deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localIDs));
+      deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
     }
   } else {
-    deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localIDs));
+    deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
   }
   if (deletedIDs.isNotEmpty) {
     final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs);
@@ -229,3 +250,42 @@ Future<List<String>> _deleteLocalFilesInBatches(
   Navigator.of(dialogKey.currentContext, rootNavigator: true).pop('dialog');
   return deletedIDs;
 }
+
+
+Future<bool> _localFileExist(File file) {
+  if (file.isSharedMediaToAppSandbox()) {
+    var localFile = io.File(getSharedMediaFilePath(file));
+    return localFile.exists();
+  } else {
+    return file.getAsset().then((asset) {
+      if (asset == null) {
+        return false;
+      }
+      return asset.exists;
+    });
+  }
+}
+
+Future<List<String>> _tryDeleteSharedMediaFiles(List<String> localIDs) {
+  final List<String> actuallyDeletedIDs = [];
+  try {
+    return Future.forEach(localIDs, (id) async {
+      String localPath = Configuration.instance.getSharedMediaCacheDirectory() +
+          "/" +
+          id.replaceAll(kSharedMediaIdentifier, '');
+      try {
+        await io.File(localPath).delete();
+        actuallyDeletedIDs.add(id);
+      } catch (e, s) {
+        _logger.warning("Could not delete file " + id, e, s);
+        // server log shouldn't contain localId
+        _logger.severe("Could not delete file ", e, s);
+      }
+    }).then((ignore) {
+      return actuallyDeletedIDs;
+    });
+  } catch (e, s) {
+    _logger.severe("Unexpected error while deleting share media files", e, s);
+    return Future.value(actuallyDeletedIDs);
+  }
+}

+ 1 - 1
lib/utils/file_uploader.dart

@@ -385,7 +385,7 @@ class FileUploader {
 
   Future _onInvalidFileError(File file) async {
     _logger.warning("Invalid file encountered: " + file.toString());
-    await FilesDB.instance.deleteLocalFile(file.localID);
+    await FilesDB.instance.deleteLocalFile(file);
     await LocalSyncService.instance.trackInvalidFile(file);
     throw InvalidFileError();
   }

+ 37 - 2
lib/utils/file_uploader_util.dart

@@ -23,8 +23,11 @@ class MediaUploadData {
 }
 
 Future<MediaUploadData> getUploadDataFromEnteFile(ente.File file) async {
-  // todo: add local to get data from either Asset/photoManager or app cache
-  return await _getMediaUploadDataFromAssetFile(file);
+  if (file.isSharedMediaToAppSandbox()) {
+    return await _getMediaUploadDataFromAppCache(file);
+  } else {
+    return await _getMediaUploadDataFromAssetFile(file);
+  }
 }
 
 Future<MediaUploadData> _getMediaUploadDataFromAssetFile(ente.File file) async {
@@ -92,3 +95,35 @@ Future<void> _decorateEnteFileData(ente.File file, AssetEntity asset) async {
     file.title = await asset.titleAsync;
   }
 }
+
+Future<MediaUploadData> _getMediaUploadDataFromAppCache(ente.File file) async {
+  io.File sourceFile;
+  Uint8List thumbnailData;
+  bool isDeleted = false;
+  var localPath = getSharedMediaFilePath(file);
+  sourceFile = io.File(localPath);
+  if (!sourceFile.existsSync()) {
+    _logger.warning("File doesn't exist in app sandbox");
+    throw InvalidFileError();
+  }
+  thumbnailData = await getThumbnailFromInAppCacheFile(file);
+  return MediaUploadData(sourceFile, thumbnailData, isDeleted);
+}
+
+Future<Uint8List> getThumbnailFromInAppCacheFile(ente.File file) async {
+  var localFile = io.File(getSharedMediaFilePath(file));
+  if (!localFile.existsSync()) {
+    return null;
+  }
+  var thumbnailData = localFile.readAsBytesSync();
+  int compressionAttempts = 0;
+  while (thumbnailData.length > kThumbnailDataLimit &&
+      compressionAttempts < kMaximumThumbnailCompressionAttempts) {
+    _logger.info("Thumbnail size " + thumbnailData.length.toString());
+    thumbnailData = await compressThumbnail(thumbnailData);
+    _logger
+        .info("Compressed thumbnail size " + thumbnailData.length.toString());
+    compressionAttempts++;
+  }
+  return thumbnailData;
+}

+ 24 - 18
lib/utils/file_util.dart

@@ -9,7 +9,6 @@ import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:logging/logging.dart';
 import 'package:path/path.dart';
 import 'package:photos/core/cache/image_cache.dart';
-import 'package:photos/core/cache/thumbnail_cache.dart';
 import 'package:photos/core/cache/video_cache_manager.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/constants.dart';
@@ -20,6 +19,7 @@ import 'package:photos/services/collections_service.dart';
 import 'package:photos/utils/thumbnail_util.dart';
 
 import 'crypto_util.dart';
+import 'file_uploader_util.dart';
 
 final _logger = Logger("FileUtil");
 
@@ -36,7 +36,7 @@ Future<io.File> getFile(ente.File file) async {
   } else {
     final cachedFile = FileLruCache.get(file);
     if (cachedFile == null) {
-      final diskFile = await (await file.getAsset()).file;
+      final diskFile = await _getLocalDiskFile(file);
       FileLruCache.put(file, diskFile);
       return diskFile;
     }
@@ -44,26 +44,32 @@ Future<io.File> getFile(ente.File file) async {
   }
 }
 
+Future<io.File> _getLocalDiskFile(ente.File file) async {
+  if (file.isSharedMediaToAppSandbox()) {
+    var localFile = io.File(getSharedMediaFilePath(file));
+    return localFile.exists().then((exist) {
+      return exist ? localFile : null;
+    });
+  } else {
+    return file.getAsset().then((asset) async {
+      if (asset == null || !(await asset.exists)) {
+        return null;
+      }
+      return asset.file;
+    });
+  }
+}
+
+String getSharedMediaFilePath(ente.File file) {
+  return Configuration.instance.getSharedMediaCacheDirectory()
+      + "/" + file.localID.replaceAll(kSharedMediaIdentifier, '');
+}
+
 void preloadThumbnail(ente.File file) {
   if (file.isRemoteFile()) {
     getThumbnailFromServer(file);
   } else {
-    if (ThumbnailLruCache.get(file, kThumbnailSmallSize) != null) {
-      return;
-    }
-    file.getAsset().then((asset) {
-      if (asset != null) {
-        asset
-            .thumbDataWithSize(
-          kThumbnailSmallSize,
-          kThumbnailSmallSize,
-          quality: kThumbnailQuality,
-        )
-            .then((data) {
-          ThumbnailLruCache.put(file, data, kThumbnailSmallSize);
-        });
-      }
-    });
+    getThumbnailFromLocal(file);
   }
 }
 

+ 73 - 0
lib/utils/share_util.dart

@@ -1,9 +1,23 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:logging/logging.dart';
+import 'package:intl/intl.dart';
+import 'package:path/path.dart';
+import 'dart:io' as dartio;
+import 'package:exif/exif.dart';
+import 'package:photos/core/constants.dart';
+import 'package:photos/models/file_type.dart';
+import 'package:receive_sharing_intent/receive_sharing_intent.dart';
 import 'package:share/share.dart';
+import 'package:photos/core/configuration.dart';
 import 'package:flutter/widgets.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/file_util.dart';
 
+DateFormat _exifDateFormat = DateFormat('yyyy:MM:dd HH:mm:ss');
+final _logger = Logger("ShareUtil");
+// share is used to share media/files from ente to other apps
 Future<void> share(BuildContext context, List<File> files) async {
   final dialog = createProgressDialog(context, "preparing...");
   await dialog.show();
@@ -19,3 +33,62 @@ Future<void> share(BuildContext context, List<File> files) async {
 Future<void> shareText(String text) async {
   return Share.share(text);
 }
+
+Future<List<File>> convertIncomingSharedMediaToFile(
+    List<SharedMediaFile> sharedMedia, int collectionID) async {
+  List<File> localFiles = [];
+  for (var media in sharedMedia) {
+    var enteFile = File();
+    // fileName: img_x.jpg
+    enteFile.title = basename(media.path);
+
+    var ioFile = dartio.File(media.path);
+    ioFile = ioFile.renameSync(Configuration.instance.getSharedMediaCacheDirectory() +
+        "/" +
+        enteFile.title);
+    enteFile.localID = kSharedMediaIdentifier + enteFile.title;
+    enteFile.collectionID = collectionID;
+    enteFile.fileType = FileType.image;
+
+    var exifMap = await readExifFromFile(ioFile);
+    if (exifMap != null &&
+        exifMap["Image DateTime"] != null &&
+        '0000:00:00 00:00:00' != exifMap["Image DateTime"].toString()) {
+      try {
+        final exifTime =
+            _exifDateFormat.parse(exifMap["Image DateTime"].toString());
+        enteFile.creationTime = exifTime.microsecondsSinceEpoch;
+      } catch (e) {
+        //ignore
+      }
+    }
+    if (enteFile.creationTime == null || enteFile.creationTime == 0) {
+      final parsedDateTime =
+          parseDateFromFileName(basenameWithoutExtension(media.path));
+      if (parsedDateTime != null) {
+        enteFile.creationTime = parsedDateTime.microsecondsSinceEpoch;
+      } else {
+        enteFile.creationTime = DateTime.now().microsecondsSinceEpoch;
+      }
+    }
+    enteFile.modificationTime = enteFile.creationTime;
+    localFiles.add(enteFile);
+  }
+  return localFiles;
+}
+
+DateTime parseDateFromFileName(String fileName) {
+  if (fileName.startsWith('IMG-') || fileName.startsWith('VID-')) {
+    // Whatsapp media files
+    return DateTime.tryParse(fileName.split('-')[1]);
+  } else if (fileName.startsWith("Screenshot_")) {
+    // Screenshots on droid
+    return DateTime.tryParse(
+        (fileName).replaceAll('Screenshot_', '').replaceAll('-', 'T'));
+  } else {
+    return DateTime.tryParse((fileName)
+        .replaceAll("IMG_", "")
+        .replaceAll("DCIM_", "")
+        .replaceAll("_", " "));
+  }
+}

+ 37 - 0
lib/utils/thumbnail_util.dart

@@ -15,6 +15,9 @@ import 'package:photos/models/file.dart';
 import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/file_util.dart';
 
+import 'dart:io' as io;
+import 'file_uploader_util.dart';
+
 final _logger = Logger("ThumbnailUtil");
 final _map = <int, FileDownloadItem>{};
 final _queue = Queue<int>();
@@ -55,6 +58,40 @@ Future<Uint8List> getThumbnailFromServer(File file) async {
   }
 }
 
+Future<Uint8List> getThumbnailFromLocal(File file,
+    {int size = kThumbnailSmallSize, int quality = kThumbnailQuality}) async {
+  if (ThumbnailLruCache.get(file, size) != null) {
+    return ThumbnailLruCache.get(file);
+  }
+  final cachedThumbnail = getCachedThumbnail(file);
+  if (cachedThumbnail.existsSync()) {
+    final data = cachedThumbnail.readAsBytesSync();
+    ThumbnailLruCache.put(file, data);
+    return data;
+  }
+  if (file.isSharedMediaToAppSandbox()) {
+    //todo:neeraj support specifying size/quality
+    return getThumbnailFromInAppCacheFile(file).then((data) {
+      if (data != null) {
+        ThumbnailLruCache.put(file, data, size);
+      }
+      return data;
+    });
+  } else {
+    return file.getAsset().then((asset) async {
+      if (asset == null || !(await asset.exists)) {
+        return null;
+      }
+      return asset
+          .thumbDataWithSize(size, size, quality: quality)
+          .then((data) {
+        ThumbnailLruCache.put(file, data, size);
+        return data;
+      });
+    });
+  }
+}
+
 void removePendingGetThumbnailRequestIfAny(File file) {
   if (_map.containsKey(file.uploadedFileID)) {
     final item = _map[file.uploadedFileID];

+ 7 - 0
pubspec.lock

@@ -746,6 +746,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.0.5"
+  receive_sharing_intent:
+    dependency: "direct main"
+    description:
+      name: receive_sharing_intent
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.4.5"
   rxdart:
     dependency: transitive
     description:

+ 1 - 0
pubspec.yaml

@@ -76,6 +76,7 @@ dependencies:
   pinput: ^1.2.0
   provider: ^3.1.0
   quiver: ^3.0.1
+  receive_sharing_intent: ^1.4.5
   scrollable_positioned_list: ^0.1.10
   sentry: ^5.0.0
   share: ^2.0.1