diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 09ab72502..40524d203 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,34 +1,46 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ package="deckers.thibault.aves"
+ android:installLocation="auto">
+
+
+
+
-
-
-
+
+
-
-
+
+
+
+
+
+
+
+
-
diff --git a/android/app/src/main/java/deckers/thibault/aves/MainActivity.java b/android/app/src/main/java/deckers/thibault/aves/MainActivity.java
index 1494c95bd..0fc854452 100644
--- a/android/app/src/main/java/deckers/thibault/aves/MainActivity.java
+++ b/android/app/src/main/java/deckers/thibault/aves/MainActivity.java
@@ -3,6 +3,10 @@ package deckers.thibault.aves;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
import deckers.thibault.aves.channelhandlers.AppAdapterHandler;
import deckers.thibault.aves.channelhandlers.ImageFileHandler;
@@ -11,6 +15,7 @@ import deckers.thibault.aves.channelhandlers.MetadataHandler;
import deckers.thibault.aves.utils.Constants;
import deckers.thibault.aves.utils.Env;
import deckers.thibault.aves.utils.PermissionManager;
+import deckers.thibault.aves.utils.Utils;
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel;
@@ -18,11 +23,19 @@ import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.view.FlutterView;
public class MainActivity extends FlutterActivity {
+ private static final String LOG_TAG = Utils.createLogTag(MainActivity.class);
+
+ public static final String VIEWER_CHANNEL = "deckers.thibault/aves/viewer";
+
+ private Map sharedEntryMap;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
+ handleIntent(getIntent());
+
MediaStoreStreamHandler mediaStoreStreamHandler = new MediaStoreStreamHandler();
FlutterView messenger = getFlutterView();
@@ -30,6 +43,27 @@ public class MainActivity extends FlutterActivity {
new MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(new ImageFileHandler(this, mediaStoreStreamHandler));
new MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(new MetadataHandler(this));
new EventChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandler(mediaStoreStreamHandler);
+
+ new MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler(
+ (call, result) -> {
+ if (call.method.contentEquals("getSharedEntry")) {
+ result.success(sharedEntryMap);
+ sharedEntryMap = null;
+ }
+ });
+ }
+
+ private void handleIntent(Intent intent) {
+ Log.i(LOG_TAG, "handleIntent intent=" + intent);
+ if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) {
+ Uri uri = intent.getData();
+ String mimeType = intent.getType();
+ if (uri != null && mimeType != null) {
+ sharedEntryMap = new HashMap<>();
+ sharedEntryMap.put("uri", uri.toString());
+ sharedEntryMap.put("mimeType", mimeType);
+ }
+ }
}
@Override
diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java
index fdff87275..b0dd3ba4c 100644
--- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java
+++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java
@@ -35,6 +35,9 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
mediaStoreStreamHandler.fetchAll(activity);
result.success(null);
break;
+ case "getImageEntry":
+ getImageEntry(call, result);
+ break;
case "getImageBytes":
getImageBytes(call, result);
break;
@@ -74,6 +77,34 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
result.success(null);
}
+ private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+ String uriString = call.argument("uri");
+ String mimeType = call.argument("mimeType");
+ if (uriString == null || mimeType == null) {
+ result.error("getImageEntry-args", "failed because of missing arguments", null);
+ return;
+ }
+
+ Uri uri = Uri.parse(uriString);
+ ImageProvider provider = ImageProviderFactory.getProvider(uri);
+ if (provider == null) {
+ result.error("getImageEntry-provider", "failed to find provider for uri=" + uriString, null);
+ return;
+ }
+
+ provider.fetchSingle(activity, uri, mimeType, new ImageProvider.ImageOpCallback() {
+ @Override
+ public void onSuccess(Map entry) {
+ result.success(entry);
+ }
+
+ @Override
+ public void onFailure() {
+ result.error("getImageEntry-failure", "failed to get entry for uri=" + uriString, null);
+ }
+ });
+ }
+
private void delete(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Map entryMap = call.argument("entry");
if (entryMap == null) {
diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java
index 5a4f1ac2f..1cff8c103 100644
--- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java
+++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java
@@ -5,9 +5,7 @@ import android.util.Log;
import java.time.Duration;
import java.time.Instant;
-import java.util.stream.Stream;
-import deckers.thibault.aves.model.ImageEntry;
import deckers.thibault.aves.model.provider.MediaStoreImageProvider;
import deckers.thibault.aves.utils.Utils;
import io.flutter.plugin.common.EventChannel;
diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java
index e0f151138..81f35d661 100644
--- a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java
+++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java
@@ -33,6 +33,10 @@ import deckers.thibault.aves.utils.Utils;
public abstract class ImageProvider {
private static final String LOG_TAG = Utils.createLogTag(ImageProvider.class);
+ public void fetchSingle(final Activity activity, final Uri uri, final String mimeType, final ImageOpCallback callback) {
+ callback.onFailure();
+ }
+
public void delete(final Activity activity, final String path, final Uri uri, final ImageOpCallback callback) {
callback.onFailure();
}
diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java
index d1397aafa..f21406f0a 100644
--- a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java
+++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java
@@ -12,9 +12,11 @@ public class ImageProviderFactory {
if (scheme != null) {
switch (scheme) {
case ContentResolver.SCHEME_CONTENT: // content://
- String authority = uri.getAuthority();
- if (authority != null) {
- switch (authority) {
+ // a URI's authority is [userinfo@]host[:port]
+ // but we only want the host when comparing to MediaStore's "authority"
+ String host = uri.getHost();
+ if (host != null) {
+ switch (host) {
case MediaStore.AUTHORITY:
return new MediaStoreImageProvider();
// case Constants.DOWNLOADS_AUTHORITY:
diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java
index 8c9d2a4f0..b9d21cd36 100644
--- a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java
+++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java
@@ -8,8 +8,10 @@ import android.provider.MediaStore;
import android.util.Log;
import java.util.HashMap;
+import java.util.Map;
import java.util.stream.Stream;
+import deckers.thibault.aves.utils.Constants;
import deckers.thibault.aves.utils.Env;
import deckers.thibault.aves.utils.PermissionManager;
import deckers.thibault.aves.utils.StorageUtils;
@@ -38,15 +40,32 @@ public class MediaStoreImageProvider extends ImageProvider {
}).flatMap(Stream::of).toArray(String[]::new);
public void fetchAll(Activity activity, EventChannel.EventSink entrySink) {
- fetch(activity, entrySink, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION);
- fetch(activity, entrySink, MediaStore.Video.Media.EXTERNAL_CONTENT_URI, VIDEO_PROJECTION);
+ fetchFrom(activity, entrySink::success, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION, null, null);
+ fetchFrom(activity, entrySink::success, MediaStore.Video.Media.EXTERNAL_CONTENT_URI, VIDEO_PROJECTION, null, null);
}
- private void fetch(final Activity activity, EventChannel.EventSink entrySink, final Uri contentUri, String[] projection) {
+ @Override
+ public void fetchSingle(final Activity activity, final Uri uri, final String mimeType, final ImageOpCallback callback) {
+ long id = ContentUris.parseId(uri);
+ String selection = MediaStore.MediaColumns._ID + "=?";
+ String[] selectionArgs = new String[]{String.valueOf(id)};
+ int entryCount = 0;
+ if (mimeType.startsWith(Constants.MIME_IMAGE)) {
+ entryCount = fetchFrom(activity, callback::onSuccess, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs);
+ } else if (mimeType.startsWith(Constants.MIME_VIDEO)) {
+ entryCount = fetchFrom(activity, callback::onSuccess, MediaStore.Video.Media.EXTERNAL_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs);
+ }
+ if (entryCount == 0) {
+ callback.onFailure();
+ }
+ }
+
+ private int fetchFrom(final Activity activity, NewEntryHandler newEntryHandler, final Uri contentUri, String[] projection, String selection, String[] selectionArgs) {
String orderBy = MediaStore.MediaColumns.DATE_TAKEN + " DESC";
+ int entryCount = 0;
try {
- Cursor cursor = activity.getContentResolver().query(contentUri, projection, null, null, orderBy);
+ Cursor cursor = activity.getContentResolver().query(contentUri, projection, selection, selectionArgs, orderBy);
if (cursor != null) {
// image & video
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID);
@@ -74,7 +93,7 @@ public class MediaStoreImageProvider extends ImageProvider {
// 2) extract actual mimeType with metadata-extractor
// 3) update MediaStore
if (width > 0) {
- entrySink.success(
+ newEntryHandler.handleEntry(
new HashMap() {{
put("uri", itemUri.toString());
put("path", cursor.getString(pathColumn));
@@ -90,6 +109,7 @@ public class MediaStoreImageProvider extends ImageProvider {
put("bucketDisplayName", cursor.getString(bucketDisplayNameColumn));
put("durationMillis", durationColumn != -1 ? cursor.getLong(durationColumn) : 0);
}});
+ entryCount++;
// } else {
// // some images are incorrectly registered in the MediaStore,
// // they are valid but miss some attributes, such as width, height, orientation
@@ -107,6 +127,7 @@ public class MediaStoreImageProvider extends ImageProvider {
} catch (Exception e) {
Log.e(LOG_TAG, "failed to get entries", e);
}
+ return entryCount;
}
@Override
@@ -139,4 +160,8 @@ public class MediaStoreImageProvider extends ImageProvider {
callback.onFailure();
}
+
+ private interface NewEntryHandler {
+ void handleEntry(Map entry);
+ }
}
\ No newline at end of file
diff --git a/android/app/src/main/java/deckers/thibault/aves/utils/Constants.java b/android/app/src/main/java/deckers/thibault/aves/utils/Constants.java
index 73e26fd06..e1ddbf248 100644
--- a/android/app/src/main/java/deckers/thibault/aves/utils/Constants.java
+++ b/android/app/src/main/java/deckers/thibault/aves/utils/Constants.java
@@ -14,6 +14,7 @@ public class Constants {
public static final String MIME_JPEG = "image/jpeg";
public static final String MIME_PNG = "image/png";
public static final String MIME_MP2T = "video/mp2t"; // .m2ts
+ public static final String MIME_IMAGE = "image";
public static final String MIME_VIDEO = "video";
// video metadata keys, from android.media.MediaMetadataRetriever
diff --git a/lib/main.dart b/lib/main.dart
index b80a2695b..4824b1dbb 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,9 +1,13 @@
+import 'package:aves/model/image_entry.dart';
+import 'package:aves/model/image_file_service.dart';
import 'package:aves/model/settings.dart';
import 'package:aves/utils/android_file_utils.dart';
+import 'package:aves/utils/viewer_service.dart';
import 'package:aves/widgets/album/all_collection_drawer.dart';
import 'package:aves/widgets/album/all_collection_page.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/providers/media_store_collection_provider.dart';
+import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
@@ -47,6 +51,7 @@ class HomePage extends StatefulWidget {
}
class _HomePageState extends State {
+ ImageEntry _sharedEntry;
Future _appSetup;
@override
@@ -60,6 +65,7 @@ class _HomePageState extends State {
Future _setup() async {
debugPrint('$runtimeType _setup');
+
// TODO reduce permission check time
final permissions = await PermissionHandler().requestPermissions([
PermissionGroup.storage,
@@ -75,6 +81,11 @@ class _HomePageState extends State {
await androidFileUtils.init(); // 170ms
await settings.init(); // <20ms
+
+ final sharedExtra = await ViewerService.getSharedEntry();
+ if (sharedExtra != null) {
+ _sharedEntry = await ImageFileService.getImageEntry(sharedExtra['uri'], sharedExtra['mimeType']);
+ }
}
@override
@@ -86,7 +97,11 @@ class _HomePageState extends State {
if (snapshot.hasError) return const Icon(OMIcons.error);
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
debugPrint('$runtimeType FutureBuilder builder');
- return const MediaStoreCollectionPage();
+ return _sharedEntry != null
+ ? SingleFullscreenPage(
+ entry: _sharedEntry,
+ )
+ : const MediaStoreCollectionPage();
}),
);
}
diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart
index ae7f58c05..ac8fc47ff 100644
--- a/lib/model/image_entry.dart
+++ b/lib/model/image_entry.dart
@@ -111,7 +111,7 @@ class ImageEntry {
return width / height;
}
- int get megaPixels => (width * height / 1000000).round();
+ int get megaPixels => width != null && height != null ? (width * height / 1000000).round() : null;
DateTime get bestDate {
if ((catalogMetadata?.dateMillis ?? 0) > 0) return DateTime.fromMillisecondsSinceEpoch(catalogMetadata.dateMillis);
diff --git a/lib/model/image_file_service.dart b/lib/model/image_file_service.dart
index e19491e90..208bda5b0 100644
--- a/lib/model/image_file_service.dart
+++ b/lib/model/image_file_service.dart
@@ -15,6 +15,20 @@ class ImageFileService {
}
}
+ static Future getImageEntry(String uri, String mimeType) async {
+ debugPrint('getImageEntry for uri=$uri, mimeType=$mimeType');
+ try {
+ final result = await platform.invokeMethod('getImageEntry', {
+ 'uri': uri,
+ 'mimeType': mimeType,
+ }) as Map;
+ return ImageEntry.fromMap(result);
+ } on PlatformException catch (e) {
+ debugPrint('getImageEntry failed with exception=${e.message}');
+ }
+ return null;
+ }
+
static Future getImageBytes(ImageEntry entry, int width, int height) async {
if (width > 0 && height > 0) {
// debugPrint('getImageBytes width=$width path=${entry.path}');
diff --git a/lib/utils/viewer_service.dart b/lib/utils/viewer_service.dart
new file mode 100644
index 000000000..ac848ec35
--- /dev/null
+++ b/lib/utils/viewer_service.dart
@@ -0,0 +1,16 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+class ViewerService {
+ static const platform = MethodChannel('deckers.thibault/aves/viewer');
+
+ static Future