├── .npmignore ├── README.md ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── me │ └── naxel │ └── RNCameraRollAndroid │ ├── RNCameraRollAndroidModule.java │ └── RNCameraRollAndroidPackage.java ├── index.js └── package.json /.npmignore: -------------------------------------------------------------------------------- 1 | .git -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-camera-roll-android 2 | 3 | [Documentation](https://facebook.github.io/react-native/docs/cameraroll) 4 | 5 | #### Why? 6 | 7 | Fixed issue https://github.com/facebook/react-native/issues/20112 8 | 9 | #### Supports 10 | Android 11 | 12 | ### Installation 13 | ``` 14 | react-native link react-native-camera-roll-android 15 | ``` 16 | #### Manual 17 | `android/settings.gradle`: 18 | settings.gradle 19 | ```diff 20 | + include ':react-native-camera-roll-android' 21 | + project(':react-native-camera-roll-android').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-camera-roll-android/android') 22 | 23 | ``` 24 | 25 | `android/app/build.gradle`: 26 | 27 | ```diff 28 | dependencies { 29 | ... 30 | + implementation project(':react-native-camera-roll-android') 31 | ... 32 | } 33 | ``` 34 | 35 | `android/app/src/.../MainApplication.java`: 36 | 37 | ```diff 38 | dependencies { 39 | ... 40 | import android.app.Application; 41 | ... 42 | +import me.naxel.RNCameraRollAndroid.RNCameraRollAndroidPackage; 43 | ... 44 | protected List getPackages() { 45 | return Arrays.asList( 46 | new MainReactPackage(), 47 | + new RNCameraRollAndroidPackage(), 48 | ... 49 | ); 50 | } 51 | ``` 52 | 53 | 54 | ### Using 55 | ```js 56 | import { CameraRoll as CameraRollIOS, Platform } from "react-native"; 57 | import CameraRollAndroid from 'react-native-camera-roll-android'; 58 | 59 | if (Platform.OS === 'android') { 60 | CameraRoll = CameraRollAndroid; 61 | } else { 62 | CameraRoll = CameraRollIOS; 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.3.0' 8 | } 9 | } 10 | 11 | apply plugin: 'com.android.library' 12 | 13 | android { 14 | compileSdkVersion 27 15 | buildToolsVersion "27.0.3" 16 | 17 | defaultConfig { 18 | minSdkVersion 16 19 | targetSdkVersion 26 20 | versionCode 1 21 | versionName "1.0" 22 | } 23 | lintOptions { 24 | abortOnError false 25 | } 26 | } 27 | 28 | repositories { 29 | mavenCentral() 30 | } 31 | 32 | dependencies { 33 | compile 'com.facebook.react:react-native:+' 34 | } 35 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/java/me/naxel/RNCameraRollAndroid/RNCameraRollAndroidModule.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package me.naxel.RNCameraRollAndroid; 9 | 10 | import android.content.ContentResolver; 11 | import android.content.Context; 12 | import android.content.res.AssetFileDescriptor; 13 | import android.database.Cursor; 14 | import android.graphics.BitmapFactory; 15 | import android.media.MediaMetadataRetriever; 16 | import android.media.MediaScannerConnection; 17 | import android.net.Uri; 18 | import android.os.AsyncTask; 19 | import android.os.Build; 20 | import android.os.Environment; 21 | import android.provider.MediaStore; 22 | import android.provider.MediaStore.Images; 23 | import android.provider.MediaStore.Video; 24 | import android.text.TextUtils; 25 | import com.facebook.common.logging.FLog; 26 | import com.facebook.react.bridge.GuardedAsyncTask; 27 | import com.facebook.react.bridge.JSApplicationIllegalArgumentException; 28 | import com.facebook.react.bridge.NativeModule; 29 | import com.facebook.react.bridge.Promise; 30 | import com.facebook.react.bridge.ReactApplicationContext; 31 | import com.facebook.react.bridge.ReactContext; 32 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 33 | import com.facebook.react.bridge.ReactMethod; 34 | import com.facebook.react.bridge.ReadableArray; 35 | import com.facebook.react.bridge.ReadableMap; 36 | import com.facebook.react.bridge.WritableArray; 37 | import com.facebook.react.bridge.WritableMap; 38 | import com.facebook.react.bridge.WritableNativeArray; 39 | import com.facebook.react.bridge.WritableNativeMap; 40 | import com.facebook.react.common.ReactConstants; 41 | import com.facebook.react.module.annotations.ReactModule; 42 | import java.io.File; 43 | import java.io.FileInputStream; 44 | import java.io.FileOutputStream; 45 | import java.io.IOException; 46 | import java.nio.channels.FileChannel; 47 | import java.util.ArrayList; 48 | import java.util.List; 49 | import javax.annotation.Nullable; 50 | 51 | // TODO #6015104: rename to something less iOSish 52 | /** 53 | * {@link NativeModule} that allows JS to interact with the photos on the device (i.e. 54 | * {@link MediaStore.Images}). 55 | */ 56 | @ReactModule(name = RNCameraRollAndroidModule.NAME) 57 | public class RNCameraRollAndroidModule extends ReactContextBaseJavaModule { 58 | 59 | protected static final String NAME = "RNCameraRollAndroid"; 60 | 61 | private static final String ERROR_UNABLE_TO_LOAD = "E_UNABLE_TO_LOAD"; 62 | private static final String ERROR_UNABLE_TO_LOAD_PERMISSION = "E_UNABLE_TO_LOAD_PERMISSION"; 63 | private static final String ERROR_UNABLE_TO_SAVE = "E_UNABLE_TO_SAVE"; 64 | 65 | public static final boolean IS_JELLY_BEAN_OR_LATER = 66 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; 67 | 68 | private static final String[] PROJECTION; 69 | static { 70 | if (IS_JELLY_BEAN_OR_LATER) { 71 | PROJECTION = new String[] { 72 | Images.Media._ID, 73 | Images.Media.MIME_TYPE, 74 | Images.Media.BUCKET_DISPLAY_NAME, 75 | Images.Media.DATE_TAKEN, 76 | Images.Media.WIDTH, 77 | Images.Media.HEIGHT, 78 | Images.Media.LONGITUDE, 79 | Images.Media.LATITUDE 80 | }; 81 | } else { 82 | PROJECTION = new String[] { 83 | Images.Media._ID, 84 | Images.Media.MIME_TYPE, 85 | Images.Media.BUCKET_DISPLAY_NAME, 86 | Images.Media.DATE_TAKEN, 87 | Images.Media.LONGITUDE, 88 | Images.Media.LATITUDE 89 | }; 90 | } 91 | } 92 | 93 | private static final String SELECTION_BUCKET = Images.Media.BUCKET_DISPLAY_NAME + " = ?"; 94 | private static final String SELECTION_DATE_TAKEN = Images.Media.DATE_TAKEN + " < ?"; 95 | 96 | public RNCameraRollAndroidModule(ReactApplicationContext reactContext) { 97 | super(reactContext); 98 | } 99 | 100 | @Override 101 | public String getName() { 102 | return NAME; 103 | } 104 | 105 | /** 106 | * Save an image to the gallery (i.e. {@link MediaStore.Images}). This copies the original file 107 | * from wherever it may be to the external storage pictures directory, so that it can be scanned 108 | * by the MediaScanner. 109 | * 110 | * @param uri the file:// URI of the image to save 111 | * @param promise to be resolved or rejected 112 | */ 113 | @ReactMethod 114 | public void saveToCameraRoll(String uri, String type, Promise promise) { 115 | new SaveToCameraRoll(getReactApplicationContext(), Uri.parse(uri), promise) 116 | .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 117 | } 118 | 119 | private static class SaveToCameraRoll extends GuardedAsyncTask { 120 | 121 | private final Context mContext; 122 | private final Uri mUri; 123 | private final Promise mPromise; 124 | 125 | public SaveToCameraRoll(ReactContext context, Uri uri, Promise promise) { 126 | super(context); 127 | mContext = context; 128 | mUri = uri; 129 | mPromise = promise; 130 | } 131 | 132 | @Override 133 | protected void doInBackgroundGuarded(Void... params) { 134 | File source = new File(mUri.getPath()); 135 | FileChannel input = null, output = null; 136 | try { 137 | File exportDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); 138 | exportDir.mkdirs(); 139 | if (!exportDir.isDirectory()) { 140 | mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available"); 141 | return; 142 | } 143 | File dest = new File(exportDir, source.getName()); 144 | int n = 0; 145 | String fullSourceName = source.getName(); 146 | String sourceName, sourceExt; 147 | if (fullSourceName.indexOf('.') >= 0) { 148 | sourceName = fullSourceName.substring(0, fullSourceName.lastIndexOf('.')); 149 | sourceExt = fullSourceName.substring(fullSourceName.lastIndexOf('.')); 150 | } else { 151 | sourceName = fullSourceName; 152 | sourceExt = ""; 153 | } 154 | while (!dest.createNewFile()) { 155 | dest = new File(exportDir, sourceName + "_" + (n++) + sourceExt); 156 | } 157 | input = new FileInputStream(source).getChannel(); 158 | output = new FileOutputStream(dest).getChannel(); 159 | output.transferFrom(input, 0, input.size()); 160 | input.close(); 161 | output.close(); 162 | 163 | MediaScannerConnection.scanFile( 164 | mContext, 165 | new String[]{dest.getAbsolutePath()}, 166 | null, 167 | new MediaScannerConnection.OnScanCompletedListener() { 168 | @Override 169 | public void onScanCompleted(String path, Uri uri) { 170 | if (uri != null) { 171 | mPromise.resolve(uri.toString()); 172 | } else { 173 | mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery"); 174 | } 175 | } 176 | }); 177 | } catch (IOException e) { 178 | mPromise.reject(e); 179 | } finally { 180 | if (input != null && input.isOpen()) { 181 | try { 182 | input.close(); 183 | } catch (IOException e) { 184 | FLog.e(ReactConstants.TAG, "Could not close input channel", e); 185 | } 186 | } 187 | if (output != null && output.isOpen()) { 188 | try { 189 | output.close(); 190 | } catch (IOException e) { 191 | FLog.e(ReactConstants.TAG, "Could not close output channel", e); 192 | } 193 | } 194 | } 195 | } 196 | } 197 | 198 | /** 199 | * Get photos from {@link MediaStore.Images}, most recent first. 200 | * 201 | * @param params a map containing the following keys: 202 | *
    203 | *
  • first (mandatory): a number representing the number of photos to fetch
  • 204 | *
  • 205 | * after (optional): a cursor that matches page_info[end_cursor] returned by a 206 | * previous call to {@link #getPhotos} 207 | *
  • 208 | *
  • groupName (optional): an album name
  • 209 | *
  • 210 | * mimeType (optional): restrict returned images to a specific mimetype (e.g. 211 | * image/jpeg) 212 | *
  • 213 | *
  • 214 | * assetType (optional): chooses between either photos or videos from the camera roll. 215 | * Valid values are "Photos" or "Videos". Defaults to photos. 216 | *
  • 217 | *
218 | * @param promise the Promise to be resolved when the photos are loaded; for a format of the 219 | * parameters passed to this callback, see {@code getPhotosReturnChecker} in CameraRoll.js 220 | */ 221 | @ReactMethod 222 | public void getPhotos(final ReadableMap params, final Promise promise) { 223 | int first = params.getInt("first"); 224 | String after = params.hasKey("after") ? params.getString("after") : null; 225 | String groupName = params.hasKey("groupName") ? params.getString("groupName") : null; 226 | String assetType = params.hasKey("assetType") ? params.getString("assetType") : null; 227 | ReadableArray mimeTypes = params.hasKey("mimeTypes") 228 | ? params.getArray("mimeTypes") 229 | : null; 230 | if (params.hasKey("groupTypes")) { 231 | throw new JSApplicationIllegalArgumentException("groupTypes is not supported on Android"); 232 | } 233 | 234 | new GetPhotosTask( 235 | getReactApplicationContext(), 236 | first, 237 | after, 238 | groupName, 239 | mimeTypes, 240 | assetType, 241 | promise) 242 | .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 243 | } 244 | 245 | private static class GetPhotosTask extends GuardedAsyncTask { 246 | private final Context mContext; 247 | private final int mFirst; 248 | private final @Nullable String mAfter; 249 | private final @Nullable String mGroupName; 250 | private final @Nullable ReadableArray mMimeTypes; 251 | private final Promise mPromise; 252 | private final @Nullable String mAssetType; 253 | 254 | private GetPhotosTask( 255 | ReactContext context, 256 | int first, 257 | @Nullable String after, 258 | @Nullable String groupName, 259 | @Nullable ReadableArray mimeTypes, 260 | @Nullable String assetType, 261 | Promise promise) { 262 | super(context); 263 | mContext = context; 264 | mFirst = first; 265 | mAfter = after; 266 | mGroupName = groupName; 267 | mMimeTypes = mimeTypes; 268 | mPromise = promise; 269 | mAssetType = assetType; 270 | } 271 | 272 | @Override 273 | protected void doInBackgroundGuarded(Void... params) { 274 | StringBuilder selection = new StringBuilder("1"); 275 | List selectionArgs = new ArrayList<>(); 276 | if (!TextUtils.isEmpty(mAfter)) { 277 | selection.append(" AND " + SELECTION_DATE_TAKEN); 278 | selectionArgs.add(mAfter); 279 | } 280 | if (!TextUtils.isEmpty(mGroupName)) { 281 | selection.append(" AND " + SELECTION_BUCKET); 282 | selectionArgs.add(mGroupName); 283 | } 284 | if (mMimeTypes != null && mMimeTypes.size() > 0) { 285 | selection.append(" AND " + Images.Media.MIME_TYPE + " IN ("); 286 | for (int i = 0; i < mMimeTypes.size(); i++) { 287 | selection.append("?,"); 288 | selectionArgs.add(mMimeTypes.getString(i)); 289 | } 290 | selection.replace(selection.length() - 1, selection.length(), ")"); 291 | } 292 | WritableMap response = new WritableNativeMap(); 293 | ContentResolver resolver = mContext.getContentResolver(); 294 | // using LIMIT in the sortOrder is not explicitly supported by the SDK (which does not support 295 | // setting a limit at all), but it works because this specific ContentProvider is backed by 296 | // an SQLite DB and forwards parameters to it without doing any parsing / validation. 297 | 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, 304 | PROJECTION, 305 | selection.toString(), 306 | selectionArgs.toArray(new String[selectionArgs.size()]), 307 | Images.Media.DATE_TAKEN + " DESC, " + Images.Media.DATE_MODIFIED + " DESC LIMIT " + 308 | (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"); 311 | } else { 312 | try { 313 | putEdges(resolver, photos, response, mFirst, mAssetType); 314 | putPageInfo(photos, response, mFirst); 315 | } finally { 316 | photos.close(); 317 | mPromise.resolve(response); 318 | } 319 | } 320 | } catch (SecurityException e) { 321 | mPromise.reject( 322 | ERROR_UNABLE_TO_LOAD_PERMISSION, 323 | "Could not get photos: need READ_EXTERNAL_STORAGE permission", 324 | e); 325 | } 326 | } 327 | } 328 | 329 | private static void putPageInfo(Cursor photos, WritableMap response, int limit) { 330 | WritableMap pageInfo = new WritableNativeMap(); 331 | pageInfo.putBoolean("has_next_page", limit < photos.getCount()); 332 | if (limit < photos.getCount()) { 333 | photos.moveToPosition(limit - 1); 334 | pageInfo.putString( 335 | "end_cursor", 336 | photos.getString(photos.getColumnIndex(Images.Media.DATE_TAKEN))); 337 | } 338 | response.putMap("page_info", pageInfo); 339 | } 340 | 341 | private static void putEdges( 342 | ContentResolver resolver, 343 | Cursor photos, 344 | WritableMap response, 345 | int limit, 346 | @Nullable String assetType) { 347 | 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++) { 359 | WritableMap edge = new WritableNativeMap(); 360 | WritableMap node = new WritableNativeMap(); 361 | boolean imageInfoSuccess = 362 | putImageInfo(resolver, photos, node, idIndex, widthIndex, heightIndex, assetType); 363 | if (imageInfoSuccess) { 364 | putBasicNodeInfo(photos, node, mimeTypeIndex, groupNameIndex, dateTakenIndex); 365 | putLocationInfo(photos, node, longitudeIndex, latitudeIndex); 366 | 367 | edge.putMap("node", node); 368 | edges.pushMap(edge); 369 | } else { 370 | // we skipped an image because we couldn't get its details (e.g. width/height), so we 371 | // decrement i in order to correctly reach the limit, if the cursor has enough rows 372 | i--; 373 | } 374 | photos.moveToNext(); 375 | } 376 | response.putArray("edges", edges); 377 | } 378 | 379 | private static void putBasicNodeInfo( 380 | Cursor photos, 381 | WritableMap node, 382 | int mimeTypeIndex, 383 | int groupNameIndex, 384 | 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); 388 | } 389 | 390 | private static boolean putImageInfo( 391 | ContentResolver resolver, 392 | Cursor photos, 393 | WritableMap node, 394 | int idIndex, 395 | int widthIndex, 396 | int heightIndex, 397 | @Nullable String assetType) { 398 | 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 | } 405 | image.putString("uri", photoUri.toString()); 406 | float width = -1; 407 | float height = -1; 408 | if (IS_JELLY_BEAN_OR_LATER) { 409 | width = photos.getInt(widthIndex); 410 | height = photos.getInt(heightIndex); 411 | } 412 | 413 | if (assetType != null 414 | && assetType.equals("Videos") 415 | && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { 416 | try { 417 | AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r"); 418 | MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 419 | retriever.setDataSource(photoDescriptor.getFileDescriptor()); 420 | 421 | try { 422 | if (width <= 0 || height <= 0) { 423 | width = 424 | Integer.parseInt( 425 | retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); 426 | height = 427 | Integer.parseInt( 428 | retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); 429 | } 430 | int timeInMillisec = 431 | Integer.parseInt( 432 | retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); 433 | int playableDuration = timeInMillisec / 1000; 434 | image.putInt("playableDuration", playableDuration); 435 | } catch (NumberFormatException e) { 436 | FLog.e( 437 | ReactConstants.TAG, 438 | "Number format exception occurred while trying to fetch video metadata for " 439 | + photoUri.toString(), 440 | e); 441 | return false; 442 | } finally { 443 | retriever.release(); 444 | photoDescriptor.close(); 445 | } 446 | } catch (Exception e) { 447 | FLog.e(ReactConstants.TAG, "Could not get video metadata for " + photoUri.toString(), e); 448 | return false; 449 | } 450 | } 451 | 452 | if (width <= 0 || height <= 0) { 453 | try { 454 | AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r"); 455 | BitmapFactory.Options options = new BitmapFactory.Options(); 456 | // Set inJustDecodeBounds to true so we don't actually load the Bitmap, but only get its 457 | // dimensions instead. 458 | options.inJustDecodeBounds = true; 459 | BitmapFactory.decodeFileDescriptor(photoDescriptor.getFileDescriptor(), null, options); 460 | width = options.outWidth; 461 | height = options.outHeight; 462 | photoDescriptor.close(); 463 | } catch (IOException e) { 464 | FLog.e(ReactConstants.TAG, "Could not get width/height for " + photoUri.toString(), e); 465 | return false; 466 | } 467 | } 468 | image.putDouble("width", width); 469 | image.putDouble("height", height); 470 | node.putMap("image", image); 471 | return true; 472 | } 473 | 474 | private static void putLocationInfo( 475 | Cursor photos, 476 | WritableMap node, 477 | int longitudeIndex, 478 | int latitudeIndex) { 479 | double longitude = photos.getDouble(longitudeIndex); 480 | double latitude = photos.getDouble(latitudeIndex); 481 | if (longitude > 0 || latitude > 0) { 482 | WritableMap location = new WritableNativeMap(); 483 | location.putDouble("longitude", longitude); 484 | location.putDouble("latitude", latitude); 485 | node.putMap("location", location); 486 | } 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /android/src/main/java/me/naxel/RNCameraRollAndroid/RNCameraRollAndroidPackage.java: -------------------------------------------------------------------------------- 1 | package me.naxel.RNCameraRollAndroid; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.NativeModule; 5 | import com.facebook.react.bridge.ReactApplicationContext; 6 | import com.facebook.react.uimanager.ViewManager; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public class RNCameraRollAndroidPackage implements ReactPackage { 13 | 14 | @Override 15 | public List createViewManagers(ReactApplicationContext reactContext) { 16 | return Collections.emptyList(); 17 | } 18 | 19 | @Override 20 | public List createNativeModules( 21 | ReactApplicationContext reactContext) { 22 | List modules = new ArrayList<>(); 23 | 24 | modules.add(new RNCameraRollAndroidModule(reactContext)); 25 | 26 | return modules; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { NativeModules } from 'react-native'; 4 | const RCTCameraRollManager = NativeModules.RNCameraRollAndroid; 5 | const invariant = require('fbjs/lib/invariant'); 6 | 7 | const GROUP_TYPES_OPTIONS = { 8 | Album: 'Album', 9 | All: 'All', 10 | Event: 'Event', 11 | Faces: 'Faces', 12 | Library: 'Library', 13 | PhotoStream: 'PhotoStream', 14 | SavedPhotos: 'SavedPhotos', // default 15 | }; 16 | 17 | const ASSET_TYPE_OPTIONS = { 18 | All: 'All', 19 | Videos: 'Videos', 20 | Photos: 'Photos', 21 | }; 22 | 23 | type GetPhotosParams = { 24 | first: number, 25 | after?: string, 26 | groupTypes?: $Keys, 27 | groupName?: string, 28 | assetType?: $Keys, 29 | mimeTypes?: Array, 30 | }; 31 | 32 | type GetPhotosReturn = Promise<{ 33 | edges: Array<{ 34 | node: { 35 | type: string, 36 | group_name: string, 37 | image: { 38 | uri: string, 39 | height: number, 40 | width: number, 41 | isStored?: boolean, 42 | playableDuration: number, 43 | }, 44 | timestamp: number, 45 | location?: { 46 | latitude?: number, 47 | longitude?: number, 48 | altitude?: number, 49 | heading?: number, 50 | speed?: number, 51 | }, 52 | }, 53 | }>, 54 | page_info: { 55 | has_next_page: boolean, 56 | start_cursor?: string, 57 | end_cursor?: string, 58 | }, 59 | }>; 60 | 61 | /** 62 | * `CameraRoll` provides access to the local camera roll or photo library. 63 | * 64 | * See https://facebook.github.io/react-native/docs/cameraroll.html 65 | */ 66 | class CameraRoll { 67 | static GroupTypesOptions: Object = GROUP_TYPES_OPTIONS; 68 | static AssetTypeOptions: Object = ASSET_TYPE_OPTIONS; 69 | 70 | /** 71 | * `CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead. 72 | */ 73 | static saveImageWithTag(tag: string): Promise { 74 | console.warn( 75 | '`CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.', 76 | ); 77 | return this.saveToCameraRoll(tag, 'photo'); 78 | } 79 | 80 | static deletePhotos(photos: Array) { 81 | return RCTCameraRollManager.deletePhotos(photos); 82 | } 83 | 84 | /** 85 | * Saves the photo or video to the camera roll or photo library. 86 | * 87 | * See https://facebook.github.io/react-native/docs/cameraroll.html#savetocameraroll 88 | */ 89 | static saveToCameraRoll( 90 | tag: string, 91 | type?: 'photo' | 'video', 92 | ): Promise { 93 | invariant( 94 | typeof tag === 'string', 95 | 'CameraRoll.saveToCameraRoll must be a valid string.', 96 | ); 97 | 98 | invariant( 99 | type === 'photo' || type === 'video' || type === undefined, 100 | `The second argument to saveToCameraRoll must be 'photo' or 'video'. You passed ${type || 101 | 'unknown'}`, 102 | ); 103 | 104 | let mediaType = 'photo'; 105 | if (type) { 106 | mediaType = type; 107 | } else if (['mov', 'mp4'].indexOf(tag.split('.').slice(-1)[0]) >= 0) { 108 | mediaType = 'video'; 109 | } 110 | 111 | return RCTCameraRollManager.saveToCameraRoll(tag, mediaType); 112 | } 113 | 114 | /** 115 | * Returns a Promise with photo identifier objects from the local camera 116 | * roll of the device 117 | * 118 | * See https://facebook.github.io/react-native/docs/cameraroll.html#getphotos 119 | */ 120 | static getPhotos(params: GetPhotosParams): GetPhotosReturn { 121 | if (arguments.length > 1) { 122 | console.warn( 123 | 'CameraRoll.getPhotos(tag, success, error) is deprecated. Use the returned Promise instead', 124 | ); 125 | let successCallback = arguments[1]; 126 | const errorCallback = arguments[2] || (() => {}); 127 | RCTCameraRollManager.getPhotos(params).then( 128 | successCallback, 129 | errorCallback, 130 | ); 131 | } 132 | // TODO: Add the __DEV__ check back in to verify the Promise result 133 | return RCTCameraRollManager.getPhotos(params); 134 | } 135 | } 136 | 137 | module.exports = CameraRoll; 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-camera-roll-android", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "react-native" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/naxel/react-native-camera-roll-android.git" 15 | }, 16 | "author": "", 17 | "license": "" 18 | } 19 | --------------------------------------------------------------------------------