Skip to content

Commit

Permalink
CameraRoll support for Videos and Photos showed in same time (#16429)
Browse files Browse the repository at this point in the history
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: #16429

Differential Revision: D13839638

Pulled By: cpojer

fbshipit-source-id: 5edc039552888c3ba8a40f39e262919fa7c00b39
  • Loading branch information
kesha-antonov authored and facebook-github-bot committed Jan 28, 2019
1 parent 11df0ea commit 54534e7
Showing 1 changed file with 90 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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;
Expand All @@ -47,10 +47,11 @@
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)
Expand All @@ -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;
Expand All @@ -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[] {
Expand All @@ -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
};
}
}
Expand Down Expand Up @@ -223,15 +232,15 @@ public void getPhotos(final ReadableMap params, final Promise promise) {
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;
if (params.hasKey("groupTypes")) {
throw new JSApplicationIllegalArgumentException("groupTypes is not supported on Android");
}

new GetPhotosTask(
new GetMediaTask(
getReactApplicationContext(),
first,
after,
Expand All @@ -242,22 +251,22 @@ public void getPhotos(final ReadableMap params, final Promise promise) {
.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 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;
Expand All @@ -281,6 +290,27 @@ protected void doInBackgroundGuarded(Void... params) {
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++) {
Expand All @@ -295,74 +325,70 @@ protected void doInBackgroundGuarded(Void... params) {
// 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);

for (int i = 0; i < limit && !photos.isAfterLast(); i++) {
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 && !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);
Expand All @@ -371,47 +397,44 @@ private static void putEdges(
// 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");
Expand Down Expand Up @@ -468,16 +491,17 @@ private static boolean putImageInfo(
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);
Expand Down

0 comments on commit 54534e7

Please sign in to comment.