20
20
import android .os .Environment ;
21
21
import android .provider .MediaStore ;
22
22
import android .provider .MediaStore .Images ;
23
- import android .provider .MediaStore .Video ;
23
+ import android .provider .MediaStore .MediaColumns ;
24
24
import android .text .TextUtils ;
25
25
import com .facebook .common .logging .FLog ;
26
26
import com .facebook .react .bridge .GuardedAsyncTask ;
47
47
import java .util .ArrayList ;
48
48
import java .util .List ;
49
49
import javax .annotation .Nullable ;
50
+ import java .net .URLConnection ;
50
51
51
52
// TODO #6015104: rename to something less iOSish
52
53
/**
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.
54
55
* {@link MediaStore.Images}).
55
56
*/
56
57
@ ReactModule (name = CameraRollManager .NAME )
@@ -61,6 +62,12 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
61
62
private static final String ERROR_UNABLE_TO_LOAD = "E_UNABLE_TO_LOAD" ;
62
63
private static final String ERROR_UNABLE_TO_LOAD_PERMISSION = "E_UNABLE_TO_LOAD_PERMISSION" ;
63
64
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
+
64
71
65
72
public static final boolean IS_JELLY_BEAN_OR_LATER =
66
73
Build .VERSION .SDK_INT >= Build .VERSION_CODES .JELLY_BEAN ;
@@ -73,10 +80,11 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
73
80
Images .Media .MIME_TYPE ,
74
81
Images .Media .BUCKET_DISPLAY_NAME ,
75
82
Images .Media .DATE_TAKEN ,
76
- Images . Media .WIDTH ,
77
- Images . Media .HEIGHT ,
83
+ MediaStore . MediaColumns .WIDTH ,
84
+ MediaStore . MediaColumns .HEIGHT ,
78
85
Images .Media .LONGITUDE ,
79
- Images .Media .LATITUDE
86
+ Images .Media .LATITUDE ,
87
+ MediaStore .MediaColumns .DATA
80
88
};
81
89
} else {
82
90
PROJECTION = new String [] {
@@ -85,7 +93,8 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
85
93
Images .Media .BUCKET_DISPLAY_NAME ,
86
94
Images .Media .DATE_TAKEN ,
87
95
Images .Media .LONGITUDE ,
88
- Images .Media .LATITUDE
96
+ Images .Media .LATITUDE ,
97
+ MediaStore .MediaColumns .DATA
89
98
};
90
99
}
91
100
}
@@ -223,15 +232,15 @@ public void getPhotos(final ReadableMap params, final Promise promise) {
223
232
int first = params .getInt ("first" );
224
233
String after = params .hasKey ("after" ) ? params .getString ("after" ) : null ;
225
234
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 ;
227
236
ReadableArray mimeTypes = params .hasKey ("mimeTypes" )
228
237
? params .getArray ("mimeTypes" )
229
238
: null ;
230
239
if (params .hasKey ("groupTypes" )) {
231
240
throw new JSApplicationIllegalArgumentException ("groupTypes is not supported on Android" );
232
241
}
233
242
234
- new GetPhotosTask (
243
+ new GetMediaTask (
235
244
getReactApplicationContext (),
236
245
first ,
237
246
after ,
@@ -242,22 +251,22 @@ public void getPhotos(final ReadableMap params, final Promise promise) {
242
251
.executeOnExecutor (AsyncTask .THREAD_POOL_EXECUTOR );
243
252
}
244
253
245
- private static class GetPhotosTask extends GuardedAsyncTask <Void , Void > {
254
+ private static class GetMediaTask extends GuardedAsyncTask <Void , Void > {
246
255
private final Context mContext ;
247
256
private final int mFirst ;
248
257
private final @ Nullable String mAfter ;
249
258
private final @ Nullable String mGroupName ;
250
259
private final @ Nullable ReadableArray mMimeTypes ;
251
260
private final Promise mPromise ;
252
- private final @ Nullable String mAssetType ;
261
+ private final String mAssetType ;
253
262
254
- private GetPhotosTask (
263
+ private GetMediaTask (
255
264
ReactContext context ,
256
265
int first ,
257
266
@ Nullable String after ,
258
267
@ Nullable String groupName ,
259
268
@ Nullable ReadableArray mimeTypes ,
260
- @ Nullable String assetType ,
269
+ String assetType ,
261
270
Promise promise ) {
262
271
super (context );
263
272
mContext = context ;
@@ -281,6 +290,27 @@ protected void doInBackgroundGuarded(Void... params) {
281
290
selection .append (" AND " + SELECTION_BUCKET );
282
291
selectionArgs .add (mGroupName );
283
292
}
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
+
284
314
if (mMimeTypes != null && mMimeTypes .size () > 0 ) {
285
315
selection .append (" AND " + Images .Media .MIME_TYPE + " IN (" );
286
316
for (int i = 0 ; i < mMimeTypes .size (); i ++) {
@@ -295,74 +325,70 @@ protected void doInBackgroundGuarded(Void... params) {
295
325
// setting a limit at all), but it works because this specific ContentProvider is backed by
296
326
// an SQLite DB and forwards parameters to it without doing any parsing / validation.
297
327
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" ),
304
330
PROJECTION ,
305
331
selection .toString (),
306
332
selectionArgs .toArray (new String [selectionArgs .size ()]),
307
333
Images .Media .DATE_TAKEN + " DESC, " + Images .Media .DATE_MODIFIED + " DESC LIMIT " +
308
334
(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 " );
311
337
} else {
312
338
try {
313
- putEdges (resolver , photos , response , mFirst , mAssetType );
314
- putPageInfo (photos , response , mFirst );
339
+ putEdges (resolver , media , response , mFirst );
340
+ putPageInfo (media , response , mFirst );
315
341
} finally {
316
- photos .close ();
342
+ media .close ();
317
343
mPromise .resolve (response );
318
344
}
319
345
}
320
346
} catch (SecurityException e ) {
321
347
mPromise .reject (
322
348
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" ,
324
350
e );
325
351
}
326
352
}
327
353
}
328
354
329
- private static void putPageInfo (Cursor photos , WritableMap response , int limit ) {
355
+ private static void putPageInfo (Cursor media , WritableMap response , int limit ) {
330
356
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 );
334
360
pageInfo .putString (
335
361
"end_cursor" ,
336
- photos .getString (photos .getColumnIndex (Images .Media .DATE_TAKEN )));
362
+ media .getString (media .getColumnIndex (Images .Media .DATE_TAKEN )));
337
363
}
338
364
response .putMap ("page_info" , pageInfo );
339
365
}
340
366
341
367
private static void putEdges (
342
368
ContentResolver resolver ,
343
- Cursor photos ,
369
+ Cursor media ,
344
370
WritableMap response ,
345
- int limit ,
346
- @ Nullable String assetType ) {
371
+ int limit ) {
347
372
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 ++) {
359
385
WritableMap edge = new WritableNativeMap ();
360
386
WritableMap node = new WritableNativeMap ();
361
387
boolean imageInfoSuccess =
362
- putImageInfo (resolver , photos , node , idIndex , widthIndex , heightIndex , assetType );
388
+ putImageInfo (resolver , media , node , idIndex , widthIndex , heightIndex , dataIndex );
363
389
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 );
366
392
367
393
edge .putMap ("node" , node );
368
394
edges .pushMap (edge );
@@ -371,47 +397,44 @@ private static void putEdges(
371
397
// decrement i in order to correctly reach the limit, if the cursor has enough rows
372
398
i --;
373
399
}
374
- photos .moveToNext ();
400
+ media .moveToNext ();
375
401
}
376
402
response .putArray ("edges" , edges );
377
403
}
378
404
379
405
private static void putBasicNodeInfo (
380
- Cursor photos ,
406
+ Cursor media ,
381
407
WritableMap node ,
382
408
int mimeTypeIndex ,
383
409
int groupNameIndex ,
384
410
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 );
388
414
}
389
415
390
416
private static boolean putImageInfo (
391
417
ContentResolver resolver ,
392
- Cursor photos ,
418
+ Cursor media ,
393
419
WritableMap node ,
394
420
int idIndex ,
395
421
int widthIndex ,
396
422
int heightIndex ,
397
- @ Nullable String assetType ) {
423
+ int dataIndex ) {
398
424
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 ));
405
426
image .putString ("uri" , photoUri .toString ());
406
427
float width = -1 ;
407
428
float height = -1 ;
408
429
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 );
411
432
}
412
433
413
- if (assetType != null
414
- && assetType .equals ("Videos" )
434
+ String mimeType = URLConnection .guessContentTypeFromName (photoUri .toString ());
435
+
436
+ if (mimeType != null
437
+ && mimeType .startsWith ("video" )
415
438
&& android .os .Build .VERSION .SDK_INT >= android .os .Build .VERSION_CODES .GINGERBREAD_MR1 ) {
416
439
try {
417
440
AssetFileDescriptor photoDescriptor = resolver .openAssetFileDescriptor (photoUri , "r" );
@@ -468,16 +491,17 @@ private static boolean putImageInfo(
468
491
image .putDouble ("width" , width );
469
492
image .putDouble ("height" , height );
470
493
node .putMap ("image" , image );
494
+
471
495
return true ;
472
496
}
473
497
474
498
private static void putLocationInfo (
475
- Cursor photos ,
499
+ Cursor media ,
476
500
WritableMap node ,
477
501
int longitudeIndex ,
478
502
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 );
481
505
if (longitude > 0 || latitude > 0 ) {
482
506
WritableMap location = new WritableNativeMap ();
483
507
location .putDouble ("longitude" , longitude );
0 commit comments