Skip to content

Android gallery partial or limited access #2179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<Media> 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<Media> 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<Void>() {
@Override
public Void call() {
initiatePicker(activity);
return null;
}
});
}

public void openCropper(final ReadableMap options, final Promise promise) {
final Activity activity = reactContext.getCurrentActivity();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,13 +491,15 @@ declare module "react-native-image-crop-picker" {
ImageOrVideo;

export function openPicker<O extends Options>(options: O): Promise<PossibleArray<O, MediaType<O>>>;
export function openAndroidPicker<O extends Options>(options: O): Promise<any>;
export function openCamera<O extends Options>(options: O): Promise<PossibleArray<O, MediaType<O>>>;
export function openCropper(options: CropperOptions): Promise<Image>;
export function clean(): Promise<void>;
export function cleanSingle(path: string): Promise<void>;

export interface ImageCropPicker {
openPicker<O extends Options>(options: O): Promise<PossibleArray<O, MediaType<O>>>;
openAndroidPicker<O extends Options>(options: O): Promise<any>;
openCamera<O extends Options>(options: O): Promise<PossibleArray<O, MediaType<O>>>;
openCropper(options: CropperOptions): Promise<Image>;
clean(): Promise<void>;
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 1 addition & 0 deletions src/NativeImageCropPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export type PickerOptions = {

export interface Spec extends TurboModule {
openPicker(options: PickerOptions): Promise<PickerResponse>;
openAndroidPicker(options: PickerOptions): Promise<any>;
openCamera(options: PickerOptions): Promise<PickerResponse>;
openCropper(options: PickerOptions): Promise<PickerResponse>;
clean(): Promise<void>;
Expand Down