diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeCameraRollManagerSpec.java b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeCameraRollManagerSpec.java
deleted file mode 100644
index 3dd691799c9..00000000000
--- a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeCameraRollManagerSpec.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- *
This source code is licensed under the MIT license found in the LICENSE file in the root
- * directory of this source tree.
- *
- *
Generated by an internal genrule from Flow types.
- *
- * @generated
- * @nolint
- */
-
-package com.facebook.fbreact.specs;
-
-import com.facebook.react.bridge.Promise;
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.ReactContextBaseJavaModule;
-import com.facebook.react.bridge.ReactMethod;
-import com.facebook.react.bridge.ReactModuleWithSpec;
-import com.facebook.react.bridge.ReadableArray;
-import com.facebook.react.bridge.ReadableMap;
-import com.facebook.react.turbomodule.core.interfaces.TurboModule;
-
-public abstract class NativeCameraRollManagerSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule {
- public NativeCameraRollManagerSpec(ReactApplicationContext reactContext) {
- super(reactContext);
- }
-
- @ReactMethod
- public abstract void getPhotos(ReadableMap params, Promise promise);
-
- @ReactMethod
- public abstract void deletePhotos(ReadableArray assets, Promise promise);
-
- @ReactMethod
- public abstract void saveToCameraRoll(String uri, String type, Promise promise);
-}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java b/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java
deleted file mode 100644
index 7220d67a96b..00000000000
--- a/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java
+++ /dev/null
@@ -1,545 +0,0 @@
-/*
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-package com.facebook.react.modules.camera;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.res.AssetFileDescriptor;
-import android.database.Cursor;
-import android.graphics.BitmapFactory;
-import android.media.MediaMetadataRetriever;
-import android.media.MediaScannerConnection;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Environment;
-import android.provider.MediaStore;
-import android.provider.MediaStore.Images;
-import android.text.TextUtils;
-import androidx.annotation.Nullable;
-import com.facebook.common.logging.FLog;
-import com.facebook.fbreact.specs.NativeCameraRollManagerSpec;
-import com.facebook.react.bridge.GuardedAsyncTask;
-import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
-import com.facebook.react.bridge.NativeModule;
-import com.facebook.react.bridge.Promise;
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.ReactContext;
-import com.facebook.react.bridge.ReadableArray;
-import com.facebook.react.bridge.ReadableMap;
-import com.facebook.react.bridge.WritableArray;
-import com.facebook.react.bridge.WritableMap;
-import com.facebook.react.bridge.WritableNativeArray;
-import com.facebook.react.bridge.WritableNativeMap;
-import com.facebook.react.common.ReactConstants;
-import com.facebook.react.module.annotations.ReactModule;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.net.URL;
-import java.nio.ByteBuffer;
-import java.nio.channels.Channels;
-import java.nio.channels.FileChannel;
-import java.nio.channels.ReadableByteChannel;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-// TODO #6015104: rename to something less iOSish
-/**
- * {@link NativeModule} that allows JS to interact with the photos and videos on the device (i.e.
- * {@link MediaStore.Images}).
- */
-@ReactModule(name = CameraRollManager.NAME)
-public class CameraRollManager extends NativeCameraRollManagerSpec {
-
- public static final String NAME = "CameraRollManager";
-
- 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";
-
- private static final String SELECTION_BUCKET = Images.Media.BUCKET_DISPLAY_NAME + " = ?";
- private static final String SELECTION_DATE_TAKEN = Images.Media.DATE_TAKEN + " < ?";
- private static final String SELECTION_MEDIA_SIZE = Images.Media.SIZE + " < ?";
-
- private static final int IMAGES_MEDIA_LATITUDE_LONGITUDE_DEPRECATED_API_LEVEL = 29;
- private static final String[] PROJECTION_LIST;
-
- static {
- ArrayList projection_list =
- new ArrayList<>(
- Arrays.asList(
- Images.Media._ID,
- Images.Media.MIME_TYPE,
- Images.Media.BUCKET_DISPLAY_NAME,
- Images.Media.DATE_TAKEN,
- MediaStore.MediaColumns.WIDTH,
- MediaStore.MediaColumns.HEIGHT,
- MediaStore.MediaColumns.DATA));
- if (Build.VERSION.SDK_INT < IMAGES_MEDIA_LATITUDE_LONGITUDE_DEPRECATED_API_LEVEL) {
- projection_list.add(Images.Media.LATITUDE);
- projection_list.add(Images.Media.LONGITUDE);
- PROJECTION_LIST = projection_list.toArray(new String[0]);
- } else {
- PROJECTION_LIST = projection_list.toArray(new String[0]);
- }
- }
-
- public CameraRollManager(ReactApplicationContext reactContext) {
- super(reactContext);
- }
-
- @Override
- public String getName() {
- return NAME;
- }
-
- /**
- * Save an image to the gallery (i.e. {@link MediaStore.Images}). This copies the original file
- * from wherever it may be to the external storage pictures directory, so that it can be scanned
- * by the MediaScanner.
- *
- * @param uri the file://, http:// or https:// URI of the image to save
- * @param promise to be resolved or rejected
- */
- @Override
- public void saveToCameraRoll(String uri, String type, Promise promise) {
- new SaveToCameraRoll(getReactApplicationContext(), Uri.parse(uri), promise)
- .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- private static class SaveToCameraRoll extends GuardedAsyncTask {
-
- private static final int SAVE_BUFFER_SIZE = 1048576; // 1MB
- private final Context mContext;
- private final Uri mUri;
- private final Promise mPromise;
-
- public SaveToCameraRoll(ReactContext context, Uri uri, Promise promise) {
- super(context);
- mContext = context;
- mUri = uri;
- mPromise = promise;
- }
-
- @Override
- protected void doInBackgroundGuarded(Void... params) {
- ReadableByteChannel input = null;
- FileChannel output = null;
- File source = new File(mUri.getPath());
- try {
- String scheme = mUri.getScheme();
- if (scheme.equals("http") || scheme.equals("https")) {
- input = Channels.newChannel(new URL(mUri.toString()).openStream());
- } else {
- input = new FileInputStream(source).getChannel();
- }
- File exportDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
- exportDir.mkdirs();
- if (!exportDir.isDirectory()) {
- mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available");
- return;
- }
- File dest = new File(exportDir, source.getName());
- int n = 0;
- String fullSourceName = source.getName();
- String sourceName, sourceExt;
- if (fullSourceName.indexOf('.') >= 0) {
- sourceName = fullSourceName.substring(0, fullSourceName.lastIndexOf('.'));
- sourceExt = fullSourceName.substring(fullSourceName.lastIndexOf('.'));
- } else {
- sourceName = fullSourceName;
- sourceExt = "";
- }
- while (!dest.createNewFile()) {
- dest = new File(exportDir, sourceName + "_" + (n++) + sourceExt);
- }
- output = new FileOutputStream(dest).getChannel();
- // Performs a buffered copy
- final ByteBuffer buffer = ByteBuffer.allocate(SAVE_BUFFER_SIZE);
- while (input.read(buffer) > 0) {
- buffer.flip();
- output.write(buffer);
- buffer.compact();
- }
- // Drains the buffer
- buffer.flip();
- while (buffer.hasRemaining()) {
- output.write(buffer);
- }
- input.close();
- output.close();
-
- MediaScannerConnection.scanFile(
- mContext,
- new String[] {dest.getAbsolutePath()},
- null,
- new MediaScannerConnection.OnScanCompletedListener() {
- @Override
- public void onScanCompleted(String path, Uri uri) {
- if (uri != null) {
- mPromise.resolve(uri.toString());
- } else {
- mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery");
- }
- }
- });
- } catch (IOException e) {
- mPromise.reject(e);
- } finally {
- if (input != null && input.isOpen()) {
- try {
- input.close();
- } catch (IOException e) {
- FLog.e(ReactConstants.TAG, "Could not close input channel", e);
- }
- }
- if (output != null && output.isOpen()) {
- try {
- output.close();
- } catch (IOException e) {
- FLog.e(ReactConstants.TAG, "Could not close output channel", e);
- }
- }
- }
- }
- }
-
- /**
- * Get photos from {@link MediaStore.Images}, most recent first.
- *
- * @param params a map containing the following keys:
- *
- * - first (mandatory): a number representing the number of photos to fetch
- *
- after (optional): a cursor that matches page_info[end_cursor] returned by a previous
- * call to {@link #getPhotos}
- *
- groupName (optional): an album name
- *
- mimeType (optional): restrict returned images to a specific mimetype (e.g.
- * image/jpeg)
- *
- assetType (optional): chooses between either photos or videos from the camera roll.
- * Valid values are "Photos" or "Videos". Defaults to photos.
- *
- *
- * @param promise the Promise to be resolved when the photos are loaded; for a format of the
- * parameters passed to this callback, see {@code getPhotosReturnChecker} in CameraRoll.js
- */
- @Override
- 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") : ASSET_TYPE_PHOTOS;
- Integer maxSize = params.hasKey("maxSize") ? params.getInt("maxSize") : null;
- ReadableArray mimeTypes = params.hasKey("mimeTypes") ? params.getArray("mimeTypes") : null;
- if (params.hasKey("groupTypes")) {
- throw new JSApplicationIllegalArgumentException("groupTypes is not supported on Android");
- }
-
- new GetMediaTask(
- getReactApplicationContext(),
- first,
- after,
- groupName,
- mimeTypes,
- assetType,
- maxSize,
- promise)
- .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- private static class GetMediaTask extends GuardedAsyncTask {
- 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 String mAssetType;
- private final @Nullable Integer mMaxSize;
-
- private GetMediaTask(
- ReactContext context,
- int first,
- @Nullable String after,
- @Nullable String groupName,
- @Nullable ReadableArray mimeTypes,
- String assetType,
- @Nullable Integer maxSize,
- Promise promise) {
- super(context);
- mContext = context;
- mFirst = first;
- mAfter = after;
- mGroupName = groupName;
- mMimeTypes = mimeTypes;
- mPromise = promise;
- mAssetType = assetType;
- mMaxSize = maxSize;
- }
-
- @Override
- protected void doInBackgroundGuarded(Void... params) {
- StringBuilder selection = new StringBuilder("1");
- List selectionArgs = new ArrayList<>();
- if (!TextUtils.isEmpty(mAfter)) {
- selection.append(" AND " + SELECTION_DATE_TAKEN);
- selectionArgs.add(mAfter);
- }
- if (!TextUtils.isEmpty(mGroupName)) {
- selection.append(" AND " + SELECTION_BUCKET);
- selectionArgs.add(mGroupName);
- }
- if (mMaxSize != null) {
- selection.append(" AND " + SELECTION_MEDIA_SIZE);
- selectionArgs.add(mMaxSize.toString());
- }
-
- switch (mAssetType) {
- case ASSET_TYPE_PHOTOS:
- selection.append(
- " AND "
- + MediaStore.Files.FileColumns.MEDIA_TYPE
- + " = "
- + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
- break;
- case ASSET_TYPE_VIDEOS:
- selection.append(
- " AND "
- + MediaStore.Files.FileColumns.MEDIA_TYPE
- + " = "
- + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
- break;
- case ASSET_TYPE_ALL:
- selection.append(
- " AND "
- + MediaStore.Files.FileColumns.MEDIA_TYPE
- + " IN ("
- + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
- + ","
- + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
- + ")");
- break;
- default:
- 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++) {
- selection.append("?,");
- selectionArgs.add(mMimeTypes.getString(i));
- }
- selection.replace(selection.length() - 1, selection.length(), ")");
- }
- WritableMap response = new WritableNativeMap();
- ContentResolver resolver = mContext.getContentResolver();
- // using LIMIT in the sortOrder is not explicitly supported by the SDK (which does not support
- // 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 {
- Cursor media =
- resolver.query(
- MediaStore.Files.getContentUri("external"),
- PROJECTION_LIST,
- 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 (media == null) {
- mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get media");
- } else {
- try {
- putEdges(resolver, media, response, mFirst);
- putPageInfo(media, response, mFirst);
- } finally {
- media.close();
- mPromise.resolve(response);
- }
- }
- } catch (SecurityException e) {
- mPromise.reject(
- ERROR_UNABLE_TO_LOAD_PERMISSION,
- "Could not get media: need READ_EXTERNAL_STORAGE permission",
- e);
- }
- }
- }
-
- private static void putPageInfo(Cursor media, WritableMap response, int limit) {
- WritableMap pageInfo = new WritableNativeMap();
- pageInfo.putBoolean("has_next_page", limit < media.getCount());
- if (limit < media.getCount()) {
- media.moveToPosition(limit - 1);
- pageInfo.putString(
- "end_cursor", media.getString(media.getColumnIndex(Images.Media.DATE_TAKEN)));
- }
- response.putMap("page_info", pageInfo);
- }
-
- private static void putEdges(
- ContentResolver resolver, Cursor media, WritableMap response, int limit) {
- WritableArray edges = new WritableNativeArray();
- 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 = media.getColumnIndex(MediaStore.MediaColumns.WIDTH);
- int heightIndex = media.getColumnIndex(MediaStore.MediaColumns.HEIGHT);
- 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, media, node, idIndex, widthIndex, heightIndex, dataIndex, mimeTypeIndex);
- if (imageInfoSuccess) {
- putBasicNodeInfo(media, node, mimeTypeIndex, groupNameIndex, dateTakenIndex);
- if (Build.VERSION.SDK_INT < IMAGES_MEDIA_LATITUDE_LONGITUDE_DEPRECATED_API_LEVEL) {
- putLocationInfo(media, node, longitudeIndex, latitudeIndex);
- }
-
- edge.putMap("node", node);
- edges.pushMap(edge);
- } else {
- // we skipped an image because we couldn't get its details (e.g. width/height), so we
- // decrement i in order to correctly reach the limit, if the cursor has enough rows
- i--;
- }
- media.moveToNext();
- }
- response.putArray("edges", edges);
- }
-
- private static void putBasicNodeInfo(
- Cursor media, WritableMap node, int mimeTypeIndex, int groupNameIndex, int dateTakenIndex) {
- 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 media,
- WritableMap node,
- int idIndex,
- int widthIndex,
- int heightIndex,
- int dataIndex,
- int mimeTypeIndex) {
- WritableMap image = new WritableNativeMap();
- Uri photoUri = Uri.parse("file://" + media.getString(dataIndex));
- image.putString("uri", photoUri.toString());
- float width = media.getInt(widthIndex);
- float height = media.getInt(heightIndex);
-
- String mimeType = media.getString(mimeTypeIndex);
-
- if (mimeType != null && mimeType.startsWith("video")) {
- try {
- AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
- retriever.setDataSource(photoDescriptor.getFileDescriptor());
-
- try {
- if (width <= 0 || height <= 0) {
- width =
- Integer.parseInt(
- retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
- height =
- Integer.parseInt(
- retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
- }
- int timeInMillisec =
- Integer.parseInt(
- retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
- int playableDuration = timeInMillisec / 1000;
- image.putInt("playableDuration", playableDuration);
- } catch (NumberFormatException e) {
- FLog.e(
- ReactConstants.TAG,
- "Number format exception occurred while trying to fetch video metadata for "
- + photoUri.toString(),
- e);
- return false;
- } finally {
- retriever.release();
- photoDescriptor.close();
- }
- } catch (Exception e) {
- FLog.e(ReactConstants.TAG, "Could not get video metadata for " + photoUri.toString(), e);
- return false;
- }
- }
-
- if (width <= 0 || height <= 0) {
- try {
- AssetFileDescriptor photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
- BitmapFactory.Options options = new BitmapFactory.Options();
- // Set inJustDecodeBounds to true so we don't actually load the Bitmap, but only get its
- // dimensions instead.
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFileDescriptor(photoDescriptor.getFileDescriptor(), null, options);
- width = options.outWidth;
- height = options.outHeight;
- photoDescriptor.close();
- } catch (IOException e) {
- FLog.e(ReactConstants.TAG, "Could not get width/height for " + photoUri.toString(), e);
- return false;
- }
- }
- image.putDouble("width", width);
- image.putDouble("height", height);
- node.putMap("image", image);
-
- return true;
- }
-
- private static void putLocationInfo(
- Cursor media, WritableMap node, int longitudeIndex, int latitudeIndex) {
- double longitude = media.getDouble(longitudeIndex);
- double latitude = media.getDouble(latitudeIndex);
- if (longitude > 0 || latitude > 0) {
- WritableMap location = new WritableNativeMap();
- location.putDouble("longitude", longitude);
- location.putDouble("latitude", latitude);
- node.putMap("location", location);
- }
- }
-
- @Override
- public void deletePhotos(ReadableArray assets, Promise promise) {
- // iOS only
- }
-}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
index 669718e63cc..35ce74f69b5 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
@@ -21,7 +21,6 @@ import com.facebook.react.modules.appearance.AppearanceModule;
import com.facebook.react.modules.appstate.AppStateModule;
import com.facebook.react.modules.blob.BlobModule;
import com.facebook.react.modules.blob.FileReaderModule;
-import com.facebook.react.modules.camera.CameraRollManager;
import com.facebook.react.modules.camera.ImageStoreManager;
import com.facebook.react.modules.clipboard.ClipboardModule;
import com.facebook.react.modules.datepicker.DatePickerDialogModule;
@@ -73,7 +72,6 @@ import java.util.Map;
BlobModule.class,
FileReaderModule.class,
AsyncStorageModule.class,
- CameraRollManager.class,
ClipboardModule.class,
DatePickerDialogModule.class,
DialogModule.class,
@@ -118,8 +116,6 @@ public class MainReactPackage extends TurboReactPackage {
return new FileReaderModule(context);
case AsyncStorageModule.NAME:
return new AsyncStorageModule(context);
- case CameraRollManager.NAME:
- return new CameraRollManager(context);
case ClipboardModule.NAME:
return new ClipboardModule(context);
case DatePickerDialogModule.FRAGMENT_TAG:
@@ -203,7 +199,6 @@ public class MainReactPackage extends TurboReactPackage {
BlobModule.class,
FileReaderModule.class,
AsyncStorageModule.class,
- CameraRollManager.class,
ClipboardModule.class,
DatePickerDialogModule.class,
DialogModule.class,