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
This commit is contained in:
Kesha Antonov 2019-01-28 07:31:29 -08:00 committed by Facebook Github Bot
parent 8556340345
commit da20713a64
1 changed files with 89 additions and 65 deletions

View File

@ -20,7 +20,7 @@ import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.provider.MediaStore.Images; import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video; import android.provider.MediaStore.MediaColumns;
import android.text.TextUtils; import android.text.TextUtils;
import com.facebook.common.logging.FLog; import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.GuardedAsyncTask; import com.facebook.react.bridge.GuardedAsyncTask;
@ -47,10 +47,11 @@ import java.nio.channels.FileChannel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.net.URLConnection;
// TODO #6015104: rename to something less iOSish // 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}). * {@link MediaStore.Images}).
*/ */
@ReactModule(name = CameraRollManager.NAME) @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 = "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_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_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 = public static final boolean IS_JELLY_BEAN_OR_LATER =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
@ -73,10 +80,11 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
Images.Media.MIME_TYPE, Images.Media.MIME_TYPE,
Images.Media.BUCKET_DISPLAY_NAME, Images.Media.BUCKET_DISPLAY_NAME,
Images.Media.DATE_TAKEN, Images.Media.DATE_TAKEN,
Images.Media.WIDTH, MediaStore.MediaColumns.WIDTH,
Images.Media.HEIGHT, MediaStore.MediaColumns.HEIGHT,
Images.Media.LONGITUDE, Images.Media.LONGITUDE,
Images.Media.LATITUDE Images.Media.LATITUDE,
MediaStore.MediaColumns.DATA
}; };
} else { } else {
PROJECTION = new String[] { PROJECTION = new String[] {
@ -85,7 +93,8 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
Images.Media.BUCKET_DISPLAY_NAME, Images.Media.BUCKET_DISPLAY_NAME,
Images.Media.DATE_TAKEN, Images.Media.DATE_TAKEN,
Images.Media.LONGITUDE, 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"); int first = params.getInt("first");
String after = params.hasKey("after") ? params.getString("after") : null; String after = params.hasKey("after") ? params.getString("after") : null;
String groupName = params.hasKey("groupName") ? params.getString("groupName") : 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") ReadableArray mimeTypes = params.hasKey("mimeTypes")
? params.getArray("mimeTypes") ? params.getArray("mimeTypes")
: null; : null;
@ -231,7 +240,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
throw new JSApplicationIllegalArgumentException("groupTypes is not supported on Android"); throw new JSApplicationIllegalArgumentException("groupTypes is not supported on Android");
} }
new GetPhotosTask( new GetMediaTask(
getReactApplicationContext(), getReactApplicationContext(),
first, first,
after, after,
@ -242,22 +251,22 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
private static class GetPhotosTask extends GuardedAsyncTask<Void, Void> { private static class GetMediaTask extends GuardedAsyncTask<Void, Void> {
private final Context mContext; private final Context mContext;
private final int mFirst; private final int mFirst;
private final @Nullable String mAfter; private final @Nullable String mAfter;
private final @Nullable String mGroupName; private final @Nullable String mGroupName;
private final @Nullable ReadableArray mMimeTypes; private final @Nullable ReadableArray mMimeTypes;
private final Promise mPromise; private final Promise mPromise;
private final @Nullable String mAssetType; private final String mAssetType;
private GetPhotosTask( private GetMediaTask(
ReactContext context, ReactContext context,
int first, int first,
@Nullable String after, @Nullable String after,
@Nullable String groupName, @Nullable String groupName,
@Nullable ReadableArray mimeTypes, @Nullable ReadableArray mimeTypes,
@Nullable String assetType, String assetType,
Promise promise) { Promise promise) {
super(context); super(context);
mContext = context; mContext = context;
@ -281,6 +290,27 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
selection.append(" AND " + SELECTION_BUCKET); selection.append(" AND " + SELECTION_BUCKET);
selectionArgs.add(mGroupName); 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) { if (mMimeTypes != null && mMimeTypes.size() > 0) {
selection.append(" AND " + Images.Media.MIME_TYPE + " IN ("); selection.append(" AND " + Images.Media.MIME_TYPE + " IN (");
for (int i = 0; i < mMimeTypes.size(); i++) { 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 // 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. // an SQLite DB and forwards parameters to it without doing any parsing / validation.
try { try {
Uri assetURI = Cursor media = resolver.query(
mAssetType != null && mAssetType.equals("Videos") ? Video.Media.EXTERNAL_CONTENT_URI : MediaStore.Files.getContentUri("external"),
Images.Media.EXTERNAL_CONTENT_URI;
Cursor photos = resolver.query(
assetURI,
PROJECTION, PROJECTION,
selection.toString(), selection.toString(),
selectionArgs.toArray(new String[selectionArgs.size()]), selectionArgs.toArray(new String[selectionArgs.size()]),
Images.Media.DATE_TAKEN + " DESC, " + Images.Media.DATE_MODIFIED + " DESC LIMIT " + 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 (mFirst + 1)); // set LIMIT to first + 1 so that we know how to populate page_info
if (photos == null) { if (media == null) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get photos"); mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get media");
} else { } else {
try { try {
putEdges(resolver, photos, response, mFirst, mAssetType); putEdges(resolver, media, response, mFirst);
putPageInfo(photos, response, mFirst); putPageInfo(media, response, mFirst);
} finally { } finally {
photos.close(); media.close();
mPromise.resolve(response); mPromise.resolve(response);
} }
} }
} catch (SecurityException e) { } catch (SecurityException e) {
mPromise.reject( mPromise.reject(
ERROR_UNABLE_TO_LOAD_PERMISSION, ERROR_UNABLE_TO_LOAD_PERMISSION,
"Could not get photos: need READ_EXTERNAL_STORAGE permission", "Could not get media: need READ_EXTERNAL_STORAGE permission",
e); 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(); WritableMap pageInfo = new WritableNativeMap();
pageInfo.putBoolean("has_next_page", limit < photos.getCount()); pageInfo.putBoolean("has_next_page", limit < media.getCount());
if (limit < photos.getCount()) { if (limit < media.getCount()) {
photos.moveToPosition(limit - 1); media.moveToPosition(limit - 1);
pageInfo.putString( pageInfo.putString(
"end_cursor", "end_cursor",
photos.getString(photos.getColumnIndex(Images.Media.DATE_TAKEN))); media.getString(media.getColumnIndex(Images.Media.DATE_TAKEN)));
} }
response.putMap("page_info", pageInfo); response.putMap("page_info", pageInfo);
} }
private static void putEdges( private static void putEdges(
ContentResolver resolver, ContentResolver resolver,
Cursor photos, Cursor media,
WritableMap response, WritableMap response,
int limit, int limit) {
@Nullable String assetType) {
WritableArray edges = new WritableNativeArray(); WritableArray edges = new WritableNativeArray();
photos.moveToFirst(); media.moveToFirst();
int idIndex = photos.getColumnIndex(Images.Media._ID); int idIndex = media.getColumnIndex(Images.Media._ID);
int mimeTypeIndex = photos.getColumnIndex(Images.Media.MIME_TYPE); int mimeTypeIndex = media.getColumnIndex(Images.Media.MIME_TYPE);
int groupNameIndex = photos.getColumnIndex(Images.Media.BUCKET_DISPLAY_NAME); int groupNameIndex = media.getColumnIndex(Images.Media.BUCKET_DISPLAY_NAME);
int dateTakenIndex = photos.getColumnIndex(Images.Media.DATE_TAKEN); int dateTakenIndex = media.getColumnIndex(Images.Media.DATE_TAKEN);
int widthIndex = IS_JELLY_BEAN_OR_LATER ? photos.getColumnIndex(Images.Media.WIDTH) : -1; int widthIndex = IS_JELLY_BEAN_OR_LATER ? media.getColumnIndex(MediaStore.MediaColumns.WIDTH) : -1;
int heightIndex = IS_JELLY_BEAN_OR_LATER ? photos.getColumnIndex(Images.Media.HEIGHT) : -1; int heightIndex = IS_JELLY_BEAN_OR_LATER ? media.getColumnIndex(MediaStore.MediaColumns.HEIGHT) : -1;
int longitudeIndex = photos.getColumnIndex(Images.Media.LONGITUDE); int longitudeIndex = media.getColumnIndex(Images.Media.LONGITUDE);
int latitudeIndex = photos.getColumnIndex(Images.Media.LATITUDE); 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 edge = new WritableNativeMap();
WritableMap node = new WritableNativeMap(); WritableMap node = new WritableNativeMap();
boolean imageInfoSuccess = boolean imageInfoSuccess =
putImageInfo(resolver, photos, node, idIndex, widthIndex, heightIndex, assetType); putImageInfo(resolver, media, node, idIndex, widthIndex, heightIndex, dataIndex);
if (imageInfoSuccess) { if (imageInfoSuccess) {
putBasicNodeInfo(photos, node, mimeTypeIndex, groupNameIndex, dateTakenIndex); putBasicNodeInfo(media, node, mimeTypeIndex, groupNameIndex, dateTakenIndex);
putLocationInfo(photos, node, longitudeIndex, latitudeIndex); putLocationInfo(media, node, longitudeIndex, latitudeIndex);
edge.putMap("node", node); edge.putMap("node", node);
edges.pushMap(edge); 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 // decrement i in order to correctly reach the limit, if the cursor has enough rows
i--; i--;
} }
photos.moveToNext(); media.moveToNext();
} }
response.putArray("edges", edges); response.putArray("edges", edges);
} }
private static void putBasicNodeInfo( private static void putBasicNodeInfo(
Cursor photos, Cursor media,
WritableMap node, WritableMap node,
int mimeTypeIndex, int mimeTypeIndex,
int groupNameIndex, int groupNameIndex,
int dateTakenIndex) { int dateTakenIndex) {
node.putString("type", photos.getString(mimeTypeIndex)); node.putString("type", media.getString(mimeTypeIndex));
node.putString("group_name", photos.getString(groupNameIndex)); node.putString("group_name", media.getString(groupNameIndex));
node.putDouble("timestamp", photos.getLong(dateTakenIndex) / 1000d); node.putDouble("timestamp", media.getLong(dateTakenIndex) / 1000d);
} }
private static boolean putImageInfo( private static boolean putImageInfo(
ContentResolver resolver, ContentResolver resolver,
Cursor photos, Cursor media,
WritableMap node, WritableMap node,
int idIndex, int idIndex,
int widthIndex, int widthIndex,
int heightIndex, int heightIndex,
@Nullable String assetType) { int dataIndex) {
WritableMap image = new WritableNativeMap(); WritableMap image = new WritableNativeMap();
Uri photoUri; Uri photoUri = Uri.parse("file://" + media.getString(dataIndex));
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));
}
image.putString("uri", photoUri.toString()); image.putString("uri", photoUri.toString());
float width = -1; float width = -1;
float height = -1; float height = -1;
if (IS_JELLY_BEAN_OR_LATER) { if (IS_JELLY_BEAN_OR_LATER) {
width = photos.getInt(widthIndex); width = media.getInt(widthIndex);
height = photos.getInt(heightIndex); height = media.getInt(heightIndex);
} }
if (assetType != null String mimeType = URLConnection.guessContentTypeFromName(photoUri.toString());
&& assetType.equals("Videos")
if (mimeType != null
&& mimeType.startsWith("video")
&& android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
try { try {
AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r"); AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
@ -468,16 +491,17 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
image.putDouble("width", width); image.putDouble("width", width);
image.putDouble("height", height); image.putDouble("height", height);
node.putMap("image", image); node.putMap("image", image);
return true; return true;
} }
private static void putLocationInfo( private static void putLocationInfo(
Cursor photos, Cursor media,
WritableMap node, WritableMap node,
int longitudeIndex, int longitudeIndex,
int latitudeIndex) { int latitudeIndex) {
double longitude = photos.getDouble(longitudeIndex); double longitude = media.getDouble(longitudeIndex);
double latitude = photos.getDouble(latitudeIndex); double latitude = media.getDouble(latitudeIndex);
if (longitude > 0 || latitude > 0) { if (longitude > 0 || latitude > 0) {
WritableMap location = new WritableNativeMap(); WritableMap location = new WritableNativeMap();
location.putDouble("longitude", longitude); location.putDouble("longitude", longitude);