From 7850dd538f82e497543b886b8cca4fe1ff5f892c Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Fri, 8 Nov 2019 00:13:24 +1100 Subject: [PATCH] feat(lib): Moved deletePhotos to use new PHAsset API and added an implementation for Android (#69) * deletePhotos works in iOS * Deletion works on Android. * Removing unnecessary commented out code. * Updated typescript typings. * Made readme more accurate based on being able to retrieve failure from the iOS API. * Let formatter run, also now rejecting the promise when there's any error on deletion on Android. --- README.md | 21 +++++ .../cameraroll/CameraRollModule.java | 77 +++++++++++++++++++ ios/RNCCameraRollManager.m | 15 ++-- js/CameraRoll.js | 9 ++- typings/CameraRoll.d.ts | 7 +- 5 files changed, 118 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index af68c958b..7198c334d 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ On Android permission is required to read the external storage. Add below line t * [`saveToCameraRoll`](#savetocameraroll) * [`save`](#save) * [`getPhotos`](#getphotos) +* [`deletePhotos`](#deletephotos) --- @@ -208,3 +209,23 @@ render() { ); } ``` +--- +### `deletePhotos()` + +```javascript +CameraRoll.deletePhotos([uri]); +``` + +Requests deletion of photos in the camera roll. + +On Android, the uri must be a local image or video URI, such as `"file:///sdcard/img.png"`. + +On iOS, the uri can be any image URI (including local, remote asset-library and base64 data URIs) or a local video file URI. The user is presented with a dialog box that shows them the asset(s) and asks them to confirm deletion. This is not able to be bypassed as per Apple Developer guidelines. + +Returns a Promise which will resolve when the deletion request is completed, or reject if there is a problem during the deletion. On iOS the user is able to cancel the deletion request, which causes a rejection, while on Android the rejection will be due to a system error. + +**Parameters:** + +| Name | Type | Required | Description | +| ---- | ---------------------- | -------- | ---------------------------------------------------------- | +| uri | string | Yes | See above. | diff --git a/android/src/main/java/com/reactnativecommunity/cameraroll/CameraRollModule.java b/android/src/main/java/com/reactnativecommunity/cameraroll/CameraRollModule.java index 7c5fc40c2..acd1a829c 100644 --- a/android/src/main/java/com/reactnativecommunity/cameraroll/CameraRollModule.java +++ b/android/src/main/java/com/reactnativecommunity/cameraroll/CameraRollModule.java @@ -8,6 +8,7 @@ package com.reactnativecommunity.cameraroll; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.database.Cursor; @@ -61,6 +62,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule { 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_DELETE = "E_UNABLE_TO_DELETE"; private static final String ERROR_UNABLE_TO_FILTER = "E_UNABLE_TO_FILTER"; private static final String ASSET_TYPE_PHOTOS = "Photos"; @@ -504,4 +506,79 @@ public class CameraRollModule extends ReactContextBaseJavaModule { node.putMap("location", location); } } + + /** + * Delete a set of images. + * + * @param uris array of file:// URIs of the images to delete + * @param promise to be resolved + */ + @ReactMethod + public void deletePhotos(ReadableArray uris, Promise promise) { + if (uris.size() == 0) { + promise.reject(ERROR_UNABLE_TO_DELETE, "Need at least one URI to delete"); + } else { + new DeletePhotos(getReactApplicationContext(), uris, promise) + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } + + private static class DeletePhotos extends GuardedAsyncTask { + + private final Context mContext; + private final ReadableArray mUris; + private final Promise mPromise; + + public DeletePhotos(ReactContext context, ReadableArray uris, Promise promise) { + super(context); + mContext = context; + mUris = uris; + mPromise = promise; + } + + @Override + protected void doInBackgroundGuarded(Void... params) { + ContentResolver resolver = mContext.getContentResolver(); + + // Set up the projection (we only need the ID) + String[] projection = { MediaStore.Images.Media._ID }; + + // Match on the file path + String innerWhere = "?"; + for (int i = 1; i < mUris.size(); i++) { + innerWhere += ", ?"; + } + + String selection = MediaStore.Images.Media.DATA + " IN (" + innerWhere + ")"; + // Query for the ID of the media matching the file path + Uri queryUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + + String[] selectionArgs = new String[mUris.size()]; + for (int i = 0; i < mUris.size(); i++) { + Uri uri = Uri.parse(mUris.getString(i)); + selectionArgs[i] = uri.getPath(); + } + + Cursor cursor = resolver.query(queryUri, projection, selection, selectionArgs, null); + int deletedCount = 0; + + while (cursor.moveToNext()) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)); + Uri deleteUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); + + if (resolver.delete(deleteUri, null, null) == 1) { + deletedCount++; + } + } + + cursor.close(); + + if (deletedCount == mUris.size()) { + mPromise.resolve(null); + } else { + mPromise.reject(ERROR_UNABLE_TO_DELETE, + "Could not delete all media, only deleted " + deletedCount + " photos."); + } + } + } } diff --git a/ios/RNCCameraRollManager.m b/ios/RNCCameraRollManager.m index 8bf9fd3ef..fb0606d03 100644 --- a/ios/RNCCameraRollManager.m +++ b/ios/RNCCameraRollManager.m @@ -367,12 +367,17 @@ RCT_EXPORT_METHOD(deletePhotos:(NSArray*)assets resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - NSArray *assets_ = [RCTConvert NSURLArray:assets]; - [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ - PHFetchResult *fetched = - [PHAsset fetchAssetsWithALAssetURLs:assets_ options:nil]; - [PHAssetChangeRequest deleteAssets:fetched]; + NSMutableArray *convertedAssets = [NSMutableArray array]; + + for (NSString *asset in assets) { + [convertedAssets addObject: [asset stringByReplacingOccurrencesOfString:@"ph://" withString:@""]]; } + + [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ + PHFetchResult *fetched = + [PHAsset fetchAssetsWithLocalIdentifiers:convertedAssets options:nil]; + [PHAssetChangeRequest deleteAssets:fetched]; + } completionHandler:^(BOOL success, NSError *error) { if (success == YES) { resolve(@(success)); diff --git a/js/CameraRoll.js b/js/CameraRoll.js index 6f74f72e6..60ceb5d65 100644 --- a/js/CameraRoll.js +++ b/js/CameraRoll.js @@ -123,8 +123,13 @@ class CameraRoll { return this.saveToCameraRoll(tag, 'photo'); } - static deletePhotos(photos: Array) { - return RNCCameraRoll.deletePhotos(photos); + /** + * On iOS: requests deletion of a set of photos from the camera roll. + * On Android: Deletes a set of photos from the camera roll. + * + */ + static deletePhotos(photoUris: Array) { + return RNCCameraRoll.deletePhotos(photoUris); } /** diff --git a/typings/CameraRoll.d.ts b/typings/CameraRoll.d.ts index bd00c3914..d3247a71a 100644 --- a/typings/CameraRoll.d.ts +++ b/typings/CameraRoll.d.ts @@ -71,11 +71,10 @@ declare namespace CameraRoll { function saveImageWithTag(tag: string): Promise; /** - * Delete a photo from the camera roll or media library. photos is an array of photo uri's. + * Delete a photo from the camera roll or media library. photoUris is an array of photo uri's. */ - function deletePhotos(photos: Array): void; - // deletePhotos: (photos: Array) => void; - + function deletePhotos(photoUris: Array): void; + /** * Saves the photo or video to the camera roll or photo library. */