diff --git a/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java b/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java index fd01e119f..314aa5c04 100644 --- a/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java +++ b/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java @@ -53,6 +53,13 @@ import java.util.UUID; import java.util.concurrent.Callable; +import android.content.ContentUris; +import android.database.Cursor; +import androidx.core.content.ContextCompat; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + class ImageCropPicker implements ActivityEventListener { static final String NAME = "RNCImageCropPicker"; @@ -399,6 +406,126 @@ public Void call() { }); } + // Android partial picker support + static class Media { + private final Uri uri; + private final String name; + private final long size; + private final String mimeType; + public Media(Uri uri, String name, long size, String mimeType) { + this.uri = uri; + this.name = name; + this.size = size; + this.mimeType = mimeType; + } + // Getters + public Uri getUri() { + return uri; + } + public String getName() { + return name; + } + public long getSize() { + return size; + } + public String getMimeType() { + return mimeType; + } + } + + // Run the querying logic in a coroutine outside of the main thread to keep the app responsive. + // Keep in mind that this code snippet is querying only images of the shared storage. + public List getImages(ContentResolver contentResolver) { + // Partial access on Android 14 (API level 34) or higher + String[] projection = new String[]{ + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.SIZE, + MediaStore.Images.Media.MIME_TYPE, + }; + Uri collectionUri; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + // Query all the device storage volumes instead of the primary only + collectionUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL); + } else { + collectionUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } + List images = new ArrayList<>(); + try (Cursor cursor = contentResolver.query( + collectionUri, + projection, + null, + null, + MediaStore.Images.Media.DATE_ADDED + " DESC")) { + if (cursor != null) { + int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID); + int displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME); + int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE); + int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE); + while (cursor.moveToNext()) { + Uri uri = ContentUris.withAppendedId(collectionUri, cursor.getLong(idColumn)); + String name = cursor.getString(displayNameColumn); + long size = cursor.getLong(sizeColumn); + String mimeType = cursor.getString(mimeTypeColumn); + Media image = new Media(uri, name, size, mimeType); + images.add(image); + } + } + } + return images; + } + + public void openAndroidPicker(final ReadableMap options, final Promise promise) { + final Activity activity = reactContext.getCurrentActivity(); + String permission = ""; + if (activity == null) { + promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); + return ; + } + + setConfiguration(options); + resultCollector.setup(promise, multiple); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + permission = Manifest.permission.READ_MEDIA_IMAGES; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this.reactContext, Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED) { + // Full access on Android 13 (API level 33) or higher + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && ContextCompat.checkSelfPermission(this.reactContext, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED) { + permission = ""; + // Partial access on Android 14 (API level 34) or higher + final var images = getImages(this.reactContext.getContentResolver()); + JSONArray jsonArray = new JSONArray(); + try { + for (Media obj : images) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("uri", obj.uri); + jsonArray.put(jsonObject); + } + } catch (JSONException e) { + e.printStackTrace(); // or handle it in another way + } + promise.resolve(jsonArray.toString()); + return ; + } else if (ContextCompat.checkSelfPermission(this.reactContext, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + // Full access up to Android 12 (API level 32) + } else { + // Access denied + Log.e("CUSTOM_MESSAGE","Access denied"); + } + if(!permission.isEmpty()) + permissionsCheck(activity, promise, Collections.singletonList(permission), new Callable() { + @Override + public Void call() { + initiatePicker(activity); + return null; + } + }); + } + public void openCropper(final ReadableMap options, final Promise promise) { final Activity activity = reactContext.getCurrentActivity(); diff --git a/android/src/newArch/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/android/src/newArch/java/com/reactnative/ivpusic/imagepicker/PickerModule.java index cb0b25f54..9807393db 100644 --- a/android/src/newArch/java/com/reactnative/ivpusic/imagepicker/PickerModule.java +++ b/android/src/newArch/java/com/reactnative/ivpusic/imagepicker/PickerModule.java @@ -20,6 +20,11 @@ public void openPicker(ReadableMap options, Promise promise) { picker.openPicker(options, promise); } + @Override + public void openAndroidPicker(ReadableMap options, Promise promise) { + picker.openAndroidPicker(options, promise); + } + @Override public void openCamera(ReadableMap options, Promise promise) { picker.openCamera(options, promise); diff --git a/android/src/oldArch/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/android/src/oldArch/java/com/reactnative/ivpusic/imagepicker/PickerModule.java index 55b59ff23..a2ec11d14 100644 --- a/android/src/oldArch/java/com/reactnative/ivpusic/imagepicker/PickerModule.java +++ b/android/src/oldArch/java/com/reactnative/ivpusic/imagepicker/PickerModule.java @@ -27,6 +27,11 @@ public void openPicker(ReadableMap options, Promise promise) { picker.openPicker(options, promise); } + @ReactMethod + public void openAndroidPicker(ReadableMap options, Promise promise) { + picker.openAndroidPicker(options, promise); + } + @ReactMethod public void openCamera(ReadableMap options, Promise promise) { picker.openCamera(options, promise); diff --git a/index.d.ts b/index.d.ts index 0ce849f7c..efeb35a20 100644 --- a/index.d.ts +++ b/index.d.ts @@ -491,6 +491,7 @@ declare module "react-native-image-crop-picker" { ImageOrVideo; export function openPicker(options: O): Promise>>; + export function openAndroidPicker(options: O): Promise; export function openCamera(options: O): Promise>>; export function openCropper(options: CropperOptions): Promise; export function clean(): Promise; @@ -498,6 +499,7 @@ declare module "react-native-image-crop-picker" { export interface ImageCropPicker { openPicker(options: O): Promise>>; + openAndroidPicker(options: O): Promise; openCamera(options: O): Promise>>; openCropper(options: CropperOptions): Promise; clean(): Promise; diff --git a/index.js b/index.js index 6cbbff117..61aee58f8 100644 --- a/index.js +++ b/index.js @@ -6,3 +6,4 @@ export const openCamera = ImageCropPicker.openCamera; export const openCropper = ImageCropPicker.openCropper; export const clean = ImageCropPicker.clean; export const cleanSingle = ImageCropPicker.cleanSingle; +export const openAndroidPicker = ImageCropPicker.openAndroidPicker; diff --git a/src/NativeImageCropPicker.ts b/src/NativeImageCropPicker.ts index 05ba9566f..00c3170a3 100644 --- a/src/NativeImageCropPicker.ts +++ b/src/NativeImageCropPicker.ts @@ -49,6 +49,7 @@ export type PickerOptions = { export interface Spec extends TurboModule { openPicker(options: PickerOptions): Promise; + openAndroidPicker(options: PickerOptions): Promise; openCamera(options: PickerOptions): Promise; openCropper(options: PickerOptions): Promise; clean(): Promise;