From 193003926132407fdf65be1729b080db620548f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Alfonso=20Mu=C3=B1oz=20Rodriguez?= Date: Thu, 9 Jul 2020 06:05:21 -0700 Subject: [PATCH] Move CameraRoll Android files to FB Internal Summary: Changelog: [Lean Core][Android] Removed CameraRoll from RN Android Reviewed By: RSNara Differential Revision: D22410115 fbshipit-source-id: 5cdb5b87a7bf5b4a8a4c2953c14ba3751b125d07 --- .../specs/NativeCameraRollManagerSpec.java | 37 -- .../modules/camera/CameraRollManager.java | 545 ------------------ .../react/shell/MainReactPackage.java | 5 - 3 files changed, 587 deletions(-) delete mode 100644 ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeCameraRollManagerSpec.java delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java 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: - *

- * - * @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,