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 1cff8c103..9850de5ba 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 @@ -3,9 +3,6 @@ package deckers.thibault.aves.channelhandlers; import android.app.Activity; import android.util.Log; -import java.time.Duration; -import java.time.Instant; - import deckers.thibault.aves.model.provider.MediaStoreImageProvider; import deckers.thibault.aves.utils.Utils; import io.flutter.plugin.common.EventChannel; @@ -29,9 +26,9 @@ public class MediaStoreStreamHandler implements EventChannel.StreamHandler { void fetchAll(Activity activity) { Log.d(LOG_TAG, "fetchAll start"); - Instant start = Instant.now(); +// Instant start = Instant.now(); new MediaStoreImageProvider().fetchAll(activity, eventSink); // 350ms eventSink.endOfStream(); - Log.d(LOG_TAG, "fetchAll complete in " + Duration.between(start, Instant.now()).toMillis() + "ms"); +// Log.d(LOG_TAG, "fetchAll complete in " + Duration.between(start, Instant.now()).toMillis() + "ms"); } } \ No newline at end of file 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 f2f8db71f..5908b4d03 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 @@ -1,9 +1,11 @@ package deckers.thibault.aves.model.provider; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.ContentUris; import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.provider.MediaStore; import android.util.Log; @@ -21,7 +23,8 @@ import io.flutter.plugin.common.EventChannel; public class MediaStoreImageProvider extends ImageProvider { private static final String LOG_TAG = Utils.createLogTag(MediaStoreImageProvider.class); - private static final String[] IMAGE_PROJECTION = { + + private static final String[] BASE_PROJECTION = { MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.MIME_TYPE, @@ -29,16 +32,28 @@ public class MediaStoreImageProvider extends ImageProvider { MediaStore.MediaColumns.TITLE, MediaStore.MediaColumns.WIDTH, MediaStore.MediaColumns.HEIGHT, - MediaStore.MediaColumns.ORIENTATION, MediaStore.MediaColumns.DATE_MODIFIED, - MediaStore.MediaColumns.DATE_TAKEN, - MediaStore.MediaColumns.BUCKET_DISPLAY_NAME, }; - private static final String[] VIDEO_PROJECTION = Stream.of(IMAGE_PROJECTION, new String[]{ - MediaStore.MediaColumns.DURATION + @SuppressLint("InlinedApi") + private static final String[] IMAGE_PROJECTION = Stream.of(BASE_PROJECTION, new String[]{ + // uses MediaStore.Images.Media instead of MediaStore.MediaColumns for APIs < Q + MediaStore.Images.Media.DATE_TAKEN, + MediaStore.Images.Media.BUCKET_DISPLAY_NAME, + MediaStore.Images.Media.ORIENTATION, }).flatMap(Stream::of).toArray(String[]::new); + @SuppressLint("InlinedApi") + private static final String[] VIDEO_PROJECTION = Stream.of(BASE_PROJECTION, new String[]{ + // uses MediaStore.Video.Media instead of MediaStore.MediaColumns for APIs < Q + MediaStore.Video.Media.DATE_TAKEN, + MediaStore.Video.Media.BUCKET_DISPLAY_NAME, + MediaStore.Video.Media.DURATION, + }, (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ? + new String[]{ + MediaStore.Video.Media.ORIENTATION, + } : new String[0]).flatMap(Stream::of).toArray(String[]::new); + public void fetchAll(Activity activity, EventChannel.EventSink entrySink) { NewEntryHandler success = entrySink::success; fetchFrom(activity, success, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION); @@ -65,6 +80,7 @@ public class MediaStoreImageProvider extends ImageProvider { } } + @SuppressLint("InlinedApi") private int fetchFrom(final Activity activity, NewEntryHandler newEntryHandler, final Uri contentUri, String[] projection) { String orderBy = MediaStore.MediaColumns.DATE_TAKEN + " DESC"; int entryCount = 0; @@ -80,10 +96,14 @@ public class MediaStoreImageProvider extends ImageProvider { int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE); int widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH); int heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT); - int orientationColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION); int dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED); + int dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN); int bucketDisplayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_DISPLAY_NAME); + + // image & video for API >= Q, only for images for API < Q + int orientationColumn = cursor.getColumnIndex(MediaStore.MediaColumns.ORIENTATION); + // video only int durationColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DURATION); @@ -91,6 +111,7 @@ public class MediaStoreImageProvider extends ImageProvider { long contentId = cursor.getLong(idColumn); // this is fine if `contentUri` does not already contain the ID Uri itemUri = ContentUris.withAppendedId(contentUri, contentId); + String path = cursor.getString(pathColumn); int width = cursor.getInt(widthColumn); // TODO TLAD sanitize mimeType // problem: some images were added as image/jpeg, but they're actually image/png @@ -102,12 +123,12 @@ public class MediaStoreImageProvider extends ImageProvider { newEntryHandler.handleEntry( new HashMap() {{ put("uri", itemUri.toString()); - put("path", cursor.getString(pathColumn)); + put("path", path); put("contentId", contentId); put("mimeType", cursor.getString(mimeTypeColumn)); put("width", width); put("height", cursor.getInt(heightColumn)); - put("orientationDegrees", cursor.getInt(orientationColumn)); + put("orientationDegrees", orientationColumn != -1 ? cursor.getInt(orientationColumn) : 0); put("sizeBytes", cursor.getLong(sizeColumn)); put("title", cursor.getString(titleColumn)); put("dateModifiedSecs", cursor.getLong(dateModifiedColumn)); @@ -116,7 +137,8 @@ public class MediaStoreImageProvider extends ImageProvider { put("durationMillis", durationColumn != -1 ? cursor.getLong(durationColumn) : 0); }}); entryCount++; -// } else { + } else { + Log.w(LOG_TAG, "failed to get size for uri=" + itemUri + ", path=" + path); // // some images are incorrectly registered in the MediaStore, // // they are valid but miss some attributes, such as width, height, orientation // try { diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index 1132b0b19..28871ddb9 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -4,7 +4,7 @@ import 'package:path/path.dart'; final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { - String externalStorage, dcimPath, downloadPath, picturesPath; + String externalStorage, dcimPath, downloadPath, moviesPath, picturesPath; static Map appNameMap = {}; @@ -15,6 +15,7 @@ class AndroidFileUtils { externalStorage = '/storage/emulated/0'; dcimPath = join(externalStorage, 'DCIM'); downloadPath = join(externalStorage, 'Download'); + moviesPath = join(externalStorage, 'Movies'); picturesPath = join(externalStorage, 'Pictures'); appNameMap = await AndroidAppService.getAppNames() ..addAll({'KakaoTalkDownload': 'com.kakao.talk'}); @@ -22,9 +23,9 @@ class AndroidFileUtils { bool isCameraPath(String path) => path != null && path.startsWith(dcimPath) && (path.endsWith('Camera') || path.endsWith('100ANDRO')); - bool isScreenshotsPath(String path) => path != null && path.startsWith(dcimPath) && path.endsWith('Screenshots'); + bool isScreenshotsPath(String path) => path != null && (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('Screenshots'); - bool isScreenRecordingsPath(String path) => path != null && path.startsWith(dcimPath) && path.endsWith('Screen recordings'); + bool isScreenRecordingsPath(String path) => path != null && (path.startsWith(dcimPath) || path.startsWith(moviesPath)) && (path.endsWith('Screen recordings') || path.endsWith('ScreenRecords')); bool isDownloadPath(String path) => path == downloadPath;