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:
parent
8556340345
commit
da20713a64
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue