From da20713a64858401bfaf0067b6659905f165a764 Mon Sep 17 00:00:00 2001 From: Kesha Antonov Date: Mon, 28 Jan 2019 07:31:29 -0800 Subject: [PATCH] CameraRoll support for Videos and Photos showed in same time (#16429) Summary: Right now you can choose to show Videos OR Photos. This PR allows to show both in the same time. [ANDROID][ENHANCEMENT] - Can show videos and photos from CameraRoll in the same time Pull Request resolved: https://github.com/facebook/react-native/pull/16429 Differential Revision: D13839638 Pulled By: cpojer fbshipit-source-id: 5edc039552888c3ba8a40f39e262919fa7c00b39 --- CameraRollManager.java | 154 ++++++++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 65 deletions(-) diff --git a/CameraRollManager.java b/CameraRollManager.java index 21b8059a9..b2a75ffb9 100644 --- a/CameraRollManager.java +++ b/CameraRollManager.java @@ -20,7 +20,7 @@ import android.os.Build; import android.os.Environment; import android.provider.MediaStore; import android.provider.MediaStore.Images; -import android.provider.MediaStore.Video; +import android.provider.MediaStore.MediaColumns; import android.text.TextUtils; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.GuardedAsyncTask; @@ -47,10 +47,11 @@ import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; +import java.net.URLConnection; // TODO #6015104: rename to something less iOSish /** - * {@link NativeModule} that allows JS to interact with the photos on the device (i.e. + * {@link NativeModule} that allows JS to interact with the photos and videos on the device (i.e. * {@link MediaStore.Images}). */ @ReactModule(name = CameraRollManager.NAME) @@ -61,6 +62,12 @@ public class CameraRollManager extends ReactContextBaseJavaModule { private static final String ERROR_UNABLE_TO_LOAD = "E_UNABLE_TO_LOAD"; private static final String ERROR_UNABLE_TO_LOAD_PERMISSION = "E_UNABLE_TO_LOAD_PERMISSION"; private static final String ERROR_UNABLE_TO_SAVE = "E_UNABLE_TO_SAVE"; + private static final String ERROR_UNABLE_TO_FILTER = "E_UNABLE_TO_FILTER"; + + private static final String ASSET_TYPE_PHOTOS = "Photos"; + private static final String ASSET_TYPE_VIDEOS = "Videos"; + private static final String ASSET_TYPE_ALL = "All"; + public static final boolean IS_JELLY_BEAN_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; @@ -73,10 +80,11 @@ public class CameraRollManager extends ReactContextBaseJavaModule { Images.Media.MIME_TYPE, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_TAKEN, - Images.Media.WIDTH, - Images.Media.HEIGHT, + MediaStore.MediaColumns.WIDTH, + MediaStore.MediaColumns.HEIGHT, Images.Media.LONGITUDE, - Images.Media.LATITUDE + Images.Media.LATITUDE, + MediaStore.MediaColumns.DATA }; } else { PROJECTION = new String[] { @@ -85,7 +93,8 @@ public class CameraRollManager extends ReactContextBaseJavaModule { Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_TAKEN, Images.Media.LONGITUDE, - Images.Media.LATITUDE + Images.Media.LATITUDE, + MediaStore.MediaColumns.DATA }; } } @@ -223,7 +232,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule { int first = params.getInt("first"); String after = params.hasKey("after") ? params.getString("after") : null; String groupName = params.hasKey("groupName") ? params.getString("groupName") : null; - String assetType = params.hasKey("assetType") ? params.getString("assetType") : null; + String assetType = params.hasKey("assetType") ? params.getString("assetType") : ASSET_TYPE_PHOTOS; ReadableArray mimeTypes = params.hasKey("mimeTypes") ? params.getArray("mimeTypes") : null; @@ -231,7 +240,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule { throw new JSApplicationIllegalArgumentException("groupTypes is not supported on Android"); } - new GetPhotosTask( + new GetMediaTask( getReactApplicationContext(), first, after, @@ -242,22 +251,22 @@ public class CameraRollManager extends ReactContextBaseJavaModule { .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - private static class GetPhotosTask extends GuardedAsyncTask { + private static class GetMediaTask extends GuardedAsyncTask { private final Context mContext; private final int mFirst; private final @Nullable String mAfter; private final @Nullable String mGroupName; private final @Nullable ReadableArray mMimeTypes; private final Promise mPromise; - private final @Nullable String mAssetType; + private final String mAssetType; - private GetPhotosTask( + private GetMediaTask( ReactContext context, int first, @Nullable String after, @Nullable String groupName, @Nullable ReadableArray mimeTypes, - @Nullable String assetType, + String assetType, Promise promise) { super(context); mContext = context; @@ -281,6 +290,27 @@ public class CameraRollManager extends ReactContextBaseJavaModule { selection.append(" AND " + SELECTION_BUCKET); selectionArgs.add(mGroupName); } + + if (mAssetType.equals(ASSET_TYPE_PHOTOS)) { + selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " = " + + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE); + } else if (mAssetType.equals(ASSET_TYPE_VIDEOS)) { + selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " = " + + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO); + } else if (mAssetType.equals(ASSET_TYPE_ALL)) { + selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " IN (" + + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO + "," + + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE + ")"); + } else { + mPromise.reject( + ERROR_UNABLE_TO_FILTER, + "Invalid filter option: '" + mAssetType + "'. Expected one of '" + + ASSET_TYPE_PHOTOS + "', '" + ASSET_TYPE_VIDEOS + "' or '" + ASSET_TYPE_ALL + "'." + ); + return; + } + + if (mMimeTypes != null && mMimeTypes.size() > 0) { selection.append(" AND " + Images.Media.MIME_TYPE + " IN ("); for (int i = 0; i < mMimeTypes.size(); i++) { @@ -295,74 +325,70 @@ public class CameraRollManager extends ReactContextBaseJavaModule { // setting a limit at all), but it works because this specific ContentProvider is backed by // an SQLite DB and forwards parameters to it without doing any parsing / validation. try { - Uri assetURI = - mAssetType != null && mAssetType.equals("Videos") ? Video.Media.EXTERNAL_CONTENT_URI : - Images.Media.EXTERNAL_CONTENT_URI; - - Cursor photos = resolver.query( - assetURI, + Cursor media = resolver.query( + MediaStore.Files.getContentUri("external"), PROJECTION, selection.toString(), selectionArgs.toArray(new String[selectionArgs.size()]), Images.Media.DATE_TAKEN + " DESC, " + Images.Media.DATE_MODIFIED + " DESC LIMIT " + (mFirst + 1)); // set LIMIT to first + 1 so that we know how to populate page_info - if (photos == null) { - mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get photos"); + if (media == null) { + mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get media"); } else { try { - putEdges(resolver, photos, response, mFirst, mAssetType); - putPageInfo(photos, response, mFirst); + putEdges(resolver, media, response, mFirst); + putPageInfo(media, response, mFirst); } finally { - photos.close(); + media.close(); mPromise.resolve(response); } } } catch (SecurityException e) { mPromise.reject( ERROR_UNABLE_TO_LOAD_PERMISSION, - "Could not get photos: need READ_EXTERNAL_STORAGE permission", + "Could not get media: need READ_EXTERNAL_STORAGE permission", e); } } } - private static void putPageInfo(Cursor photos, WritableMap response, int limit) { + private static void putPageInfo(Cursor media, WritableMap response, int limit) { WritableMap pageInfo = new WritableNativeMap(); - pageInfo.putBoolean("has_next_page", limit < photos.getCount()); - if (limit < photos.getCount()) { - photos.moveToPosition(limit - 1); + pageInfo.putBoolean("has_next_page", limit < media.getCount()); + if (limit < media.getCount()) { + media.moveToPosition(limit - 1); pageInfo.putString( "end_cursor", - photos.getString(photos.getColumnIndex(Images.Media.DATE_TAKEN))); + media.getString(media.getColumnIndex(Images.Media.DATE_TAKEN))); } response.putMap("page_info", pageInfo); } private static void putEdges( ContentResolver resolver, - Cursor photos, + Cursor media, WritableMap response, - int limit, - @Nullable String assetType) { + int limit) { WritableArray edges = new WritableNativeArray(); - photos.moveToFirst(); - int idIndex = photos.getColumnIndex(Images.Media._ID); - int mimeTypeIndex = photos.getColumnIndex(Images.Media.MIME_TYPE); - int groupNameIndex = photos.getColumnIndex(Images.Media.BUCKET_DISPLAY_NAME); - int dateTakenIndex = photos.getColumnIndex(Images.Media.DATE_TAKEN); - int widthIndex = IS_JELLY_BEAN_OR_LATER ? photos.getColumnIndex(Images.Media.WIDTH) : -1; - int heightIndex = IS_JELLY_BEAN_OR_LATER ? photos.getColumnIndex(Images.Media.HEIGHT) : -1; - int longitudeIndex = photos.getColumnIndex(Images.Media.LONGITUDE); - int latitudeIndex = photos.getColumnIndex(Images.Media.LATITUDE); + media.moveToFirst(); + int idIndex = media.getColumnIndex(Images.Media._ID); + int mimeTypeIndex = media.getColumnIndex(Images.Media.MIME_TYPE); + int groupNameIndex = media.getColumnIndex(Images.Media.BUCKET_DISPLAY_NAME); + int dateTakenIndex = media.getColumnIndex(Images.Media.DATE_TAKEN); + int widthIndex = IS_JELLY_BEAN_OR_LATER ? media.getColumnIndex(MediaStore.MediaColumns.WIDTH) : -1; + int heightIndex = IS_JELLY_BEAN_OR_LATER ? media.getColumnIndex(MediaStore.MediaColumns.HEIGHT) : -1; + int longitudeIndex = media.getColumnIndex(Images.Media.LONGITUDE); + int latitudeIndex = media.getColumnIndex(Images.Media.LATITUDE); + int dataIndex = media.getColumnIndex(MediaStore.MediaColumns.DATA); - for (int i = 0; i < limit && !photos.isAfterLast(); i++) { + for (int i = 0; i < limit && !media.isAfterLast(); i++) { WritableMap edge = new WritableNativeMap(); WritableMap node = new WritableNativeMap(); boolean imageInfoSuccess = - putImageInfo(resolver, photos, node, idIndex, widthIndex, heightIndex, assetType); + putImageInfo(resolver, media, node, idIndex, widthIndex, heightIndex, dataIndex); if (imageInfoSuccess) { - putBasicNodeInfo(photos, node, mimeTypeIndex, groupNameIndex, dateTakenIndex); - putLocationInfo(photos, node, longitudeIndex, latitudeIndex); + putBasicNodeInfo(media, node, mimeTypeIndex, groupNameIndex, dateTakenIndex); + putLocationInfo(media, node, longitudeIndex, latitudeIndex); edge.putMap("node", node); edges.pushMap(edge); @@ -371,47 +397,44 @@ public class CameraRollManager extends ReactContextBaseJavaModule { // decrement i in order to correctly reach the limit, if the cursor has enough rows i--; } - photos.moveToNext(); + media.moveToNext(); } response.putArray("edges", edges); } private static void putBasicNodeInfo( - Cursor photos, + Cursor media, WritableMap node, int mimeTypeIndex, int groupNameIndex, int dateTakenIndex) { - node.putString("type", photos.getString(mimeTypeIndex)); - node.putString("group_name", photos.getString(groupNameIndex)); - node.putDouble("timestamp", photos.getLong(dateTakenIndex) / 1000d); + node.putString("type", media.getString(mimeTypeIndex)); + node.putString("group_name", media.getString(groupNameIndex)); + node.putDouble("timestamp", media.getLong(dateTakenIndex) / 1000d); } private static boolean putImageInfo( ContentResolver resolver, - Cursor photos, + Cursor media, WritableMap node, int idIndex, int widthIndex, int heightIndex, - @Nullable String assetType) { + int dataIndex) { WritableMap image = new WritableNativeMap(); - Uri photoUri; - if (assetType != null && assetType.equals("Videos")) { - photoUri = Uri.withAppendedPath(Video.Media.EXTERNAL_CONTENT_URI, photos.getString(idIndex)); - } else { - photoUri = Uri.withAppendedPath(Images.Media.EXTERNAL_CONTENT_URI, photos.getString(idIndex)); - } + Uri photoUri = Uri.parse("file://" + media.getString(dataIndex)); image.putString("uri", photoUri.toString()); float width = -1; float height = -1; if (IS_JELLY_BEAN_OR_LATER) { - width = photos.getInt(widthIndex); - height = photos.getInt(heightIndex); + width = media.getInt(widthIndex); + height = media.getInt(heightIndex); } - if (assetType != null - && assetType.equals("Videos") + String mimeType = URLConnection.guessContentTypeFromName(photoUri.toString()); + + if (mimeType != null + && mimeType.startsWith("video") && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { try { AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r"); @@ -468,16 +491,17 @@ public class CameraRollManager extends ReactContextBaseJavaModule { image.putDouble("width", width); image.putDouble("height", height); node.putMap("image", image); + return true; } private static void putLocationInfo( - Cursor photos, + Cursor media, WritableMap node, int longitudeIndex, int latitudeIndex) { - double longitude = photos.getDouble(longitudeIndex); - double latitude = photos.getDouble(latitudeIndex); + double longitude = media.getDouble(longitudeIndex); + double latitude = media.getDouble(latitudeIndex); if (longitude > 0 || latitude > 0) { WritableMap location = new WritableNativeMap(); location.putDouble("longitude", longitude);