Skip to content

Commit 54534e7

Browse files
kesha-antonovfacebook-github-bot
authored andcommitted
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: #16429 Differential Revision: D13839638 Pulled By: cpojer fbshipit-source-id: 5edc039552888c3ba8a40f39e262919fa7c00b39
1 parent 11df0ea commit 54534e7

File tree

1 file changed

+90
-66
lines changed

1 file changed

+90
-66
lines changed

Diff for: ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java

+90-66
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import android.os.Environment;
2121
import android.provider.MediaStore;
2222
import android.provider.MediaStore.Images;
23-
import android.provider.MediaStore.Video;
23+
import android.provider.MediaStore.MediaColumns;
2424
import android.text.TextUtils;
2525
import com.facebook.common.logging.FLog;
2626
import com.facebook.react.bridge.GuardedAsyncTask;
@@ -47,10 +47,11 @@
4747
import java.util.ArrayList;
4848
import java.util.List;
4949
import javax.annotation.Nullable;
50+
import java.net.URLConnection;
5051

5152
// TODO #6015104: rename to something less iOSish
5253
/**
53-
* {@link NativeModule} that allows JS to interact with the photos on the device (i.e.
54+
* {@link NativeModule} that allows JS to interact with the photos and videos on the device (i.e.
5455
* {@link MediaStore.Images}).
5556
*/
5657
@ReactModule(name = CameraRollManager.NAME)
@@ -61,6 +62,12 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
6162
private static final String ERROR_UNABLE_TO_LOAD = "E_UNABLE_TO_LOAD";
6263
private static final String ERROR_UNABLE_TO_LOAD_PERMISSION = "E_UNABLE_TO_LOAD_PERMISSION";
6364
private static final String ERROR_UNABLE_TO_SAVE = "E_UNABLE_TO_SAVE";
65+
private static final String ERROR_UNABLE_TO_FILTER = "E_UNABLE_TO_FILTER";
66+
67+
private static final String ASSET_TYPE_PHOTOS = "Photos";
68+
private static final String ASSET_TYPE_VIDEOS = "Videos";
69+
private static final String ASSET_TYPE_ALL = "All";
70+
6471

6572
public static final boolean IS_JELLY_BEAN_OR_LATER =
6673
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
@@ -73,10 +80,11 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
7380
Images.Media.MIME_TYPE,
7481
Images.Media.BUCKET_DISPLAY_NAME,
7582
Images.Media.DATE_TAKEN,
76-
Images.Media.WIDTH,
77-
Images.Media.HEIGHT,
83+
MediaStore.MediaColumns.WIDTH,
84+
MediaStore.MediaColumns.HEIGHT,
7885
Images.Media.LONGITUDE,
79-
Images.Media.LATITUDE
86+
Images.Media.LATITUDE,
87+
MediaStore.MediaColumns.DATA
8088
};
8189
} else {
8290
PROJECTION = new String[] {
@@ -85,7 +93,8 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
8593
Images.Media.BUCKET_DISPLAY_NAME,
8694
Images.Media.DATE_TAKEN,
8795
Images.Media.LONGITUDE,
88-
Images.Media.LATITUDE
96+
Images.Media.LATITUDE,
97+
MediaStore.MediaColumns.DATA
8998
};
9099
}
91100
}
@@ -223,15 +232,15 @@ public void getPhotos(final ReadableMap params, final Promise promise) {
223232
int first = params.getInt("first");
224233
String after = params.hasKey("after") ? params.getString("after") : null;
225234
String groupName = params.hasKey("groupName") ? params.getString("groupName") : null;
226-
String assetType = params.hasKey("assetType") ? params.getString("assetType") : null;
235+
String assetType = params.hasKey("assetType") ? params.getString("assetType") : ASSET_TYPE_PHOTOS;
227236
ReadableArray mimeTypes = params.hasKey("mimeTypes")
228237
? params.getArray("mimeTypes")
229238
: null;
230239
if (params.hasKey("groupTypes")) {
231240
throw new JSApplicationIllegalArgumentException("groupTypes is not supported on Android");
232241
}
233242

234-
new GetPhotosTask(
243+
new GetMediaTask(
235244
getReactApplicationContext(),
236245
first,
237246
after,
@@ -242,22 +251,22 @@ public void getPhotos(final ReadableMap params, final Promise promise) {
242251
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
243252
}
244253

245-
private static class GetPhotosTask extends GuardedAsyncTask<Void, Void> {
254+
private static class GetMediaTask extends GuardedAsyncTask<Void, Void> {
246255
private final Context mContext;
247256
private final int mFirst;
248257
private final @Nullable String mAfter;
249258
private final @Nullable String mGroupName;
250259
private final @Nullable ReadableArray mMimeTypes;
251260
private final Promise mPromise;
252-
private final @Nullable String mAssetType;
261+
private final String mAssetType;
253262

254-
private GetPhotosTask(
263+
private GetMediaTask(
255264
ReactContext context,
256265
int first,
257266
@Nullable String after,
258267
@Nullable String groupName,
259268
@Nullable ReadableArray mimeTypes,
260-
@Nullable String assetType,
269+
String assetType,
261270
Promise promise) {
262271
super(context);
263272
mContext = context;
@@ -281,6 +290,27 @@ protected void doInBackgroundGuarded(Void... params) {
281290
selection.append(" AND " + SELECTION_BUCKET);
282291
selectionArgs.add(mGroupName);
283292
}
293+
294+
if (mAssetType.equals(ASSET_TYPE_PHOTOS)) {
295+
selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " = "
296+
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
297+
} else if (mAssetType.equals(ASSET_TYPE_VIDEOS)) {
298+
selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " = "
299+
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
300+
} else if (mAssetType.equals(ASSET_TYPE_ALL)) {
301+
selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " IN ("
302+
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO + ","
303+
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE + ")");
304+
} else {
305+
mPromise.reject(
306+
ERROR_UNABLE_TO_FILTER,
307+
"Invalid filter option: '" + mAssetType + "'. Expected one of '"
308+
+ ASSET_TYPE_PHOTOS + "', '" + ASSET_TYPE_VIDEOS + "' or '" + ASSET_TYPE_ALL + "'."
309+
);
310+
return;
311+
}
312+
313+
284314
if (mMimeTypes != null && mMimeTypes.size() > 0) {
285315
selection.append(" AND " + Images.Media.MIME_TYPE + " IN (");
286316
for (int i = 0; i < mMimeTypes.size(); i++) {
@@ -295,74 +325,70 @@ protected void doInBackgroundGuarded(Void... params) {
295325
// setting a limit at all), but it works because this specific ContentProvider is backed by
296326
// an SQLite DB and forwards parameters to it without doing any parsing / validation.
297327
try {
298-
Uri assetURI =
299-
mAssetType != null && mAssetType.equals("Videos") ? Video.Media.EXTERNAL_CONTENT_URI :
300-
Images.Media.EXTERNAL_CONTENT_URI;
301-
302-
Cursor photos = resolver.query(
303-
assetURI,
328+
Cursor media = resolver.query(
329+
MediaStore.Files.getContentUri("external"),
304330
PROJECTION,
305331
selection.toString(),
306332
selectionArgs.toArray(new String[selectionArgs.size()]),
307333
Images.Media.DATE_TAKEN + " DESC, " + Images.Media.DATE_MODIFIED + " DESC LIMIT " +
308334
(mFirst + 1)); // set LIMIT to first + 1 so that we know how to populate page_info
309-
if (photos == null) {
310-
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get photos");
335+
if (media == null) {
336+
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get media");
311337
} else {
312338
try {
313-
putEdges(resolver, photos, response, mFirst, mAssetType);
314-
putPageInfo(photos, response, mFirst);
339+
putEdges(resolver, media, response, mFirst);
340+
putPageInfo(media, response, mFirst);
315341
} finally {
316-
photos.close();
342+
media.close();
317343
mPromise.resolve(response);
318344
}
319345
}
320346
} catch (SecurityException e) {
321347
mPromise.reject(
322348
ERROR_UNABLE_TO_LOAD_PERMISSION,
323-
"Could not get photos: need READ_EXTERNAL_STORAGE permission",
349+
"Could not get media: need READ_EXTERNAL_STORAGE permission",
324350
e);
325351
}
326352
}
327353
}
328354

329-
private static void putPageInfo(Cursor photos, WritableMap response, int limit) {
355+
private static void putPageInfo(Cursor media, WritableMap response, int limit) {
330356
WritableMap pageInfo = new WritableNativeMap();
331-
pageInfo.putBoolean("has_next_page", limit < photos.getCount());
332-
if (limit < photos.getCount()) {
333-
photos.moveToPosition(limit - 1);
357+
pageInfo.putBoolean("has_next_page", limit < media.getCount());
358+
if (limit < media.getCount()) {
359+
media.moveToPosition(limit - 1);
334360
pageInfo.putString(
335361
"end_cursor",
336-
photos.getString(photos.getColumnIndex(Images.Media.DATE_TAKEN)));
362+
media.getString(media.getColumnIndex(Images.Media.DATE_TAKEN)));
337363
}
338364
response.putMap("page_info", pageInfo);
339365
}
340366

341367
private static void putEdges(
342368
ContentResolver resolver,
343-
Cursor photos,
369+
Cursor media,
344370
WritableMap response,
345-
int limit,
346-
@Nullable String assetType) {
371+
int limit) {
347372
WritableArray edges = new WritableNativeArray();
348-
photos.moveToFirst();
349-
int idIndex = photos.getColumnIndex(Images.Media._ID);
350-
int mimeTypeIndex = photos.getColumnIndex(Images.Media.MIME_TYPE);
351-
int groupNameIndex = photos.getColumnIndex(Images.Media.BUCKET_DISPLAY_NAME);
352-
int dateTakenIndex = photos.getColumnIndex(Images.Media.DATE_TAKEN);
353-
int widthIndex = IS_JELLY_BEAN_OR_LATER ? photos.getColumnIndex(Images.Media.WIDTH) : -1;
354-
int heightIndex = IS_JELLY_BEAN_OR_LATER ? photos.getColumnIndex(Images.Media.HEIGHT) : -1;
355-
int longitudeIndex = photos.getColumnIndex(Images.Media.LONGITUDE);
356-
int latitudeIndex = photos.getColumnIndex(Images.Media.LATITUDE);
357-
358-
for (int i = 0; i < limit && !photos.isAfterLast(); i++) {
373+
media.moveToFirst();
374+
int idIndex = media.getColumnIndex(Images.Media._ID);
375+
int mimeTypeIndex = media.getColumnIndex(Images.Media.MIME_TYPE);
376+
int groupNameIndex = media.getColumnIndex(Images.Media.BUCKET_DISPLAY_NAME);
377+
int dateTakenIndex = media.getColumnIndex(Images.Media.DATE_TAKEN);
378+
int widthIndex = IS_JELLY_BEAN_OR_LATER ? media.getColumnIndex(MediaStore.MediaColumns.WIDTH) : -1;
379+
int heightIndex = IS_JELLY_BEAN_OR_LATER ? media.getColumnIndex(MediaStore.MediaColumns.HEIGHT) : -1;
380+
int longitudeIndex = media.getColumnIndex(Images.Media.LONGITUDE);
381+
int latitudeIndex = media.getColumnIndex(Images.Media.LATITUDE);
382+
int dataIndex = media.getColumnIndex(MediaStore.MediaColumns.DATA);
383+
384+
for (int i = 0; i < limit && !media.isAfterLast(); i++) {
359385
WritableMap edge = new WritableNativeMap();
360386
WritableMap node = new WritableNativeMap();
361387
boolean imageInfoSuccess =
362-
putImageInfo(resolver, photos, node, idIndex, widthIndex, heightIndex, assetType);
388+
putImageInfo(resolver, media, node, idIndex, widthIndex, heightIndex, dataIndex);
363389
if (imageInfoSuccess) {
364-
putBasicNodeInfo(photos, node, mimeTypeIndex, groupNameIndex, dateTakenIndex);
365-
putLocationInfo(photos, node, longitudeIndex, latitudeIndex);
390+
putBasicNodeInfo(media, node, mimeTypeIndex, groupNameIndex, dateTakenIndex);
391+
putLocationInfo(media, node, longitudeIndex, latitudeIndex);
366392

367393
edge.putMap("node", node);
368394
edges.pushMap(edge);
@@ -371,47 +397,44 @@ private static void putEdges(
371397
// decrement i in order to correctly reach the limit, if the cursor has enough rows
372398
i--;
373399
}
374-
photos.moveToNext();
400+
media.moveToNext();
375401
}
376402
response.putArray("edges", edges);
377403
}
378404

379405
private static void putBasicNodeInfo(
380-
Cursor photos,
406+
Cursor media,
381407
WritableMap node,
382408
int mimeTypeIndex,
383409
int groupNameIndex,
384410
int dateTakenIndex) {
385-
node.putString("type", photos.getString(mimeTypeIndex));
386-
node.putString("group_name", photos.getString(groupNameIndex));
387-
node.putDouble("timestamp", photos.getLong(dateTakenIndex) / 1000d);
411+
node.putString("type", media.getString(mimeTypeIndex));
412+
node.putString("group_name", media.getString(groupNameIndex));
413+
node.putDouble("timestamp", media.getLong(dateTakenIndex) / 1000d);
388414
}
389415

390416
private static boolean putImageInfo(
391417
ContentResolver resolver,
392-
Cursor photos,
418+
Cursor media,
393419
WritableMap node,
394420
int idIndex,
395421
int widthIndex,
396422
int heightIndex,
397-
@Nullable String assetType) {
423+
int dataIndex) {
398424
WritableMap image = new WritableNativeMap();
399-
Uri photoUri;
400-
if (assetType != null && assetType.equals("Videos")) {
401-
photoUri = Uri.withAppendedPath(Video.Media.EXTERNAL_CONTENT_URI, photos.getString(idIndex));
402-
} else {
403-
photoUri = Uri.withAppendedPath(Images.Media.EXTERNAL_CONTENT_URI, photos.getString(idIndex));
404-
}
425+
Uri photoUri = Uri.parse("file://" + media.getString(dataIndex));
405426
image.putString("uri", photoUri.toString());
406427
float width = -1;
407428
float height = -1;
408429
if (IS_JELLY_BEAN_OR_LATER) {
409-
width = photos.getInt(widthIndex);
410-
height = photos.getInt(heightIndex);
430+
width = media.getInt(widthIndex);
431+
height = media.getInt(heightIndex);
411432
}
412433

413-
if (assetType != null
414-
&& assetType.equals("Videos")
434+
String mimeType = URLConnection.guessContentTypeFromName(photoUri.toString());
435+
436+
if (mimeType != null
437+
&& mimeType.startsWith("video")
415438
&& android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
416439
try {
417440
AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
@@ -468,16 +491,17 @@ private static boolean putImageInfo(
468491
image.putDouble("width", width);
469492
image.putDouble("height", height);
470493
node.putMap("image", image);
494+
471495
return true;
472496
}
473497

474498
private static void putLocationInfo(
475-
Cursor photos,
499+
Cursor media,
476500
WritableMap node,
477501
int longitudeIndex,
478502
int latitudeIndex) {
479-
double longitude = photos.getDouble(longitudeIndex);
480-
double latitude = photos.getDouble(latitudeIndex);
503+
double longitude = media.getDouble(longitudeIndex);
504+
double latitude = media.getDouble(latitudeIndex);
481505
if (longitude > 0 || latitude > 0) {
482506
WritableMap location = new WritableNativeMap();
483507
location.putDouble("longitude", longitude);

0 commit comments

Comments
 (0)