Make CameraRoll work with Promises

Summary:
public
This is the first module moving to the new model of working with Promises.

We now warn on uses of callback version.  At some point we will remove that.

Reviewed By: davidaurelio

Differential Revision: D2849811

fb-gh-sync-id: 8a31924cc2b438efc58f3ad22d5f27c273563472
This commit is contained in:
Dave Miller 2016-01-21 08:07:01 -08:00 committed by facebook-github-bot-2
parent dd6af38ad8
commit 214e86842f
3 changed files with 84 additions and 86 deletions

View File

@ -128,48 +128,43 @@ class CameraRoll {
* - a tag not matching any of the above, which means the image data will * - a tag not matching any of the above, which means the image data will
* be stored in memory (and consume memory as long as the process is alive) * be stored in memory (and consume memory as long as the process is alive)
* *
* @param successCallback Invoked with the value of `tag` on success. * Returns a Promise which when resolved will be passed the new uri
* @param errorCallback Invoked with error message on error. *
*/ */
static saveImageWithTag(tag, successCallback, errorCallback) { static saveImageWithTag(tag) {
invariant( invariant(
typeof tag === 'string', typeof tag === 'string',
'CameraRoll.saveImageWithTag tag must be a valid string.' 'CameraRoll.saveImageWithTag tag must be a valid string.'
); );
RCTCameraRollManager.saveImageWithTag( if (arguments.length > 1) {
tag, console.warn("CameraRoll.saveImageWithTag(tag, success, error) is deprecated. Use the returned Promise instead");
(imageTag) => { let successCallback = arguments[1];
successCallback && successCallback(imageTag); let errorCallback = arguments[2] || ( () => {} );
}, RCTCameraRollManager.saveImageWithTag(tag).then(successCallback, errorCallback);
(errorMessage) => { return;
errorCallback && errorCallback(errorMessage); }
}); return RCTCameraRollManager.saveImageWithTag(tag);
} }
/** /**
* Invokes `callback` with photo identifier objects from the local camera * Returns a Promise with photo identifier objects from the local camera
* roll of the device matching shape defined by `getPhotosReturnChecker`. * roll of the device matching shape defined by `getPhotosReturnChecker`.
* *
* @param {object} params See `getPhotosParamChecker`. * @param {object} params See `getPhotosParamChecker`.
* @param {function} callback Invoked with arg of shape defined by *
* `getPhotosReturnChecker` on success. * Returns a Promise which when resolved will be of shape `getPhotosReturnChecker`
* @param {function} errorCallback Invoked with error message on error. *
*/ */
static getPhotos(params, callback, errorCallback) { static getPhotos(params) {
var metaCallback = callback;
if (__DEV__) { if (__DEV__) {
getPhotosParamChecker({params}, 'params', 'CameraRoll.getPhotos'); getPhotosParamChecker({params}, 'params', 'CameraRoll.getPhotos');
invariant(
typeof callback === 'function',
'CameraRoll.getPhotos callback must be a valid function.'
);
invariant(
typeof errorCallback === 'function',
'CameraRoll.getPhotos errorCallback must be a valid function.'
);
} }
if (arguments.length > 1) {
console.warn("CameraRoll.getPhotos(tag, success, error) is deprecated. Use the returned Promise instead");
let successCallback = arguments[1];
if (__DEV__) { if (__DEV__) {
metaCallback = (response) => { let callback = arguments[1];
successCallback = (response) => {
getPhotosReturnChecker( getPhotosReturnChecker(
{response}, {response},
'response', 'response',
@ -178,7 +173,11 @@ class CameraRoll {
callback(response); callback(response);
}; };
} }
RCTCameraRollManager.getPhotos(params, metaCallback, errorCallback); let errorCallback = arguments[2] || ( () => {} );
RCTCameraRollManager.getPhotos(params).then(successCallback, errorCallback);
}
// TODO: Add the __DEV__ check back in to verify the Promise result
return RCTCameraRollManager.getPhotos(params);
} }
} }

View File

@ -36,8 +36,8 @@ import android.provider.MediaStore.Images;
import android.text.TextUtils; import android.text.TextUtils;
import com.facebook.common.logging.FLog; import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedAsyncTask; import com.facebook.react.bridge.GuardedAsyncTask;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
@ -60,6 +60,9 @@ import com.facebook.react.common.ReactConstants;
public class CameraRollManager extends ReactContextBaseJavaModule { public class CameraRollManager extends ReactContextBaseJavaModule {
private static final String TAG = "Catalyst/CameraRollManager"; private static final String TAG = "Catalyst/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";
public static final boolean IS_JELLY_BEAN_OR_LATER = public static final boolean IS_JELLY_BEAN_OR_LATER =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
@ -112,14 +115,11 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
* by the MediaScanner. * by the MediaScanner.
* *
* @param uri the file:// URI of the image to save * @param uri the file:// URI of the image to save
* @param success callback to be invoked on successful save to gallery; the only argument passed * @param promise to be resolved or rejected
* to this callback is the MediaStore content:// URI of the new image.
* @param error callback to be invoked on error (e.g. can't copy file, external storage not
* available etc.)
*/ */
@ReactMethod @ReactMethod
public void saveImageWithTag(String uri, final Callback success, final Callback error) { public void saveImageWithTag(String uri, Promise promise) {
new SaveImageTag(getReactApplicationContext(), Uri.parse(uri), success, error) new SaveImageTag(getReactApplicationContext(), Uri.parse(uri), promise)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
@ -127,15 +127,13 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
private final Context mContext; private final Context mContext;
private final Uri mUri; private final Uri mUri;
private final Callback mSuccess; private final Promise mPromise;
private final Callback mError;
public SaveImageTag(ReactContext context, Uri uri, Callback success, Callback error) { public SaveImageTag(ReactContext context, Uri uri, Promise promise) {
super(context); super(context);
mContext = context; mContext = context;
mUri = uri; mUri = uri;
mSuccess = success; mPromise = promise;
mError = error;
} }
@Override @Override
@ -147,7 +145,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
pictures.mkdirs(); pictures.mkdirs();
if (!pictures.isDirectory()) { if (!pictures.isDirectory()) {
mError.invoke("External storage pictures directory not available"); mPromise.reject(ERROR_UNABLE_TO_LOAD, "External storage pictures directory not available", null);
return; return;
} }
File dest = new File(pictures, source.getName()); File dest = new File(pictures, source.getName());
@ -178,14 +176,14 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
@Override @Override
public void onScanCompleted(String path, Uri uri) { public void onScanCompleted(String path, Uri uri) {
if (uri != null) { if (uri != null) {
mSuccess.invoke(uri.toString()); mPromise.resolve(uri.toString());
} else { } else {
mError.invoke("Could not add image to gallery"); mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery", null);
} }
} }
}); });
} catch (IOException e) { } catch (IOException e) {
mError.invoke(e.getMessage()); mPromise.reject(e);
} finally { } finally {
if (input != null && input.isOpen()) { if (input != null && input.isOpen()) {
try { try {
@ -221,12 +219,11 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
* image/jpeg) * image/jpeg)
* </li> * </li>
* </ul> * </ul>
* @param success the callback to be called when the photos are loaded; for a format of the * @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 * parameters passed to this callback, see {@code getPhotosReturnChecker} in CameraRoll.js
* @param error the callback to be called on error
*/ */
@ReactMethod @ReactMethod
public void getPhotos(final ReadableMap params, final Callback success, Callback error) { public void getPhotos(final ReadableMap params, final Promise promise) {
int first = params.getInt("first"); int first = params.getInt("first");
String after = params.hasKey("after") ? params.getString("after") : null; String after = params.hasKey("after") ? params.getString("after") : null;
String groupName = params.hasKey("groupName") ? params.getString("groupName") : null; String groupName = params.hasKey("groupName") ? params.getString("groupName") : null;
@ -243,8 +240,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
after, after,
groupName, groupName,
mimeTypes, mimeTypes,
success, promise)
error)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
@ -254,8 +250,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
private final @Nullable String mAfter; private final @Nullable String mAfter;
private final @Nullable String mGroupName; private final @Nullable String mGroupName;
private final @Nullable ReadableArray mMimeTypes; private final @Nullable ReadableArray mMimeTypes;
private final Callback mSuccess; private final Promise mPromise;
private final Callback mError;
private GetPhotosTask( private GetPhotosTask(
ReactContext context, ReactContext context,
@ -263,16 +258,14 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
@Nullable String after, @Nullable String after,
@Nullable String groupName, @Nullable String groupName,
@Nullable ReadableArray mimeTypes, @Nullable ReadableArray mimeTypes,
Callback success, Promise promise) {
Callback error) {
super(context); super(context);
mContext = context; mContext = context;
mFirst = first; mFirst = first;
mAfter = after; mAfter = after;
mGroupName = groupName; mGroupName = groupName;
mMimeTypes = mimeTypes; mMimeTypes = mimeTypes;
mSuccess = success; mPromise = promise;
mError = error;
} }
@Override @Override
@ -309,18 +302,21 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
Images.Media.DATE_TAKEN + " DESC, " + Images.Media.DATE_MODIFIED + " DESC LIMIT " + 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 (mFirst + 1)); // set LIMIT to first + 1 so that we know how to populate page_info
if (photos == null) { if (photos == null) {
mError.invoke("Could not get photos"); mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get photos", null);
} else { } else {
try { try {
putEdges(resolver, photos, response, mFirst); putEdges(resolver, photos, response, mFirst);
putPageInfo(photos, response, mFirst); putPageInfo(photos, response, mFirst);
} finally { } finally {
photos.close(); photos.close();
mSuccess.invoke(response); mPromise.resolve(response);
} }
} }
} catch (SecurityException e) { } catch (SecurityException e) {
mError.invoke("Could not get photos: need READ_EXTERNAL_STORAGE permission"); mPromise.reject(
ERROR_UNABLE_TO_LOAD_PERMISSION,
"Could not get photos: need READ_EXTERNAL_STORAGE permission",
e);
} }
} }
} }

View File

@ -79,13 +79,16 @@ RCT_EXPORT_MODULE()
@synthesize bridge = _bridge; @synthesize bridge = _bridge;
NSString *const RCTErrorUnableToLoad = @"E_UNABLE_TO_LOAD";
NSString *const RCTErrorUnableToSave = @"E_UNABLE_TO_SAVE";
RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag
successCallback:(RCTResponseSenderBlock)successCallback resolve:(RCTPromiseResolveBlock)resolve
errorCallback:(RCTResponseErrorBlock)errorCallback) reject:(RCTPromiseRejectBlock)reject)
{ {
[_bridge.imageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) { [_bridge.imageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) {
if (loadError) { if (loadError) {
errorCallback(loadError); reject(RCTErrorUnableToLoad, nil, loadError);
return; return;
} }
// It's unclear if writeImageToSavedPhotosAlbum is thread-safe // It's unclear if writeImageToSavedPhotosAlbum is thread-safe
@ -93,21 +96,21 @@ RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag
[_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { [_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) {
if (saveError) { if (saveError) {
RCTLogWarn(@"Error saving cropped image: %@", saveError); RCTLogWarn(@"Error saving cropped image: %@", saveError);
errorCallback(saveError); reject(RCTErrorUnableToSave, nil, saveError);
} else { } else {
successCallback(@[assetURL.absoluteString]); resolve(@[assetURL.absoluteString]);
} }
}]; }];
}); });
}]; }];
} }
static void RCTCallCallback(RCTResponseSenderBlock callback, static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
NSArray<NSDictionary<NSString *, id> *> *assets, NSArray<NSDictionary<NSString *, id> *> *assets,
BOOL hasNextPage) BOOL hasNextPage)
{ {
if (!assets.count) { if (!assets.count) {
callback(@[@{ resolve(@[@{
@"edges": assets, @"edges": assets,
@"page_info": @{ @"page_info": @{
@"has_next_page": @NO, @"has_next_page": @NO,
@ -115,7 +118,7 @@ static void RCTCallCallback(RCTResponseSenderBlock callback,
}]); }]);
return; return;
} }
callback(@[@{ resolve(@[@{
@"edges": assets, @"edges": assets,
@"page_info": @{ @"page_info": @{
@"start_cursor": assets[0][@"node"][@"image"][@"uri"], @"start_cursor": assets[0][@"node"][@"image"][@"uri"],
@ -126,8 +129,8 @@ static void RCTCallCallback(RCTResponseSenderBlock callback,
} }
RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
successCallback:(RCTResponseSenderBlock)successCallback resolve:(RCTPromiseResolveBlock)resolve
errorCallback:(RCTResponseErrorBlock)errorCallback) reject:(RCTPromiseRejectBlock)reject)
{ {
NSUInteger first = [RCTConvert NSInteger:params[@"first"]]; NSUInteger first = [RCTConvert NSInteger:params[@"first"]];
NSString *afterCursor = [RCTConvert NSString:params[@"after"]]; NSString *afterCursor = [RCTConvert NSString:params[@"after"]];
@ -137,7 +140,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
BOOL __block foundAfter = NO; BOOL __block foundAfter = NO;
BOOL __block hasNextPage = NO; BOOL __block hasNextPage = NO;
BOOL __block calledCallback = NO; BOOL __block resolvedPromise = NO;
NSMutableArray<NSDictionary<NSString *, id> *> *assets = [NSMutableArray new]; NSMutableArray<NSDictionary<NSString *, id> *> *assets = [NSMutableArray new];
[_bridge.assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) { [_bridge.assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) {
@ -157,9 +160,9 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
*stopAssets = YES; *stopAssets = YES;
*stopGroups = YES; *stopGroups = YES;
hasNextPage = YES; hasNextPage = YES;
RCTAssert(calledCallback == NO, @"Called the callback before we finished processing the results."); RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results.");
RCTCallCallback(successCallback, assets, hasNextPage); RCTResolvePromise(resolve, assets, hasNextPage);
calledCallback = YES; resolvedPromise = YES;
return; return;
} }
CGSize dimensions = [result defaultRepresentation].dimensions; CGSize dimensions = [result defaultRepresentation].dimensions;
@ -188,18 +191,18 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
} }
}]; }];
} else { } else {
// Sometimes the enumeration continues even if we set stop above, so we guard against calling the callback // Sometimes the enumeration continues even if we set stop above, so we guard against resolving the promise
// multiple times here. // multiple times here.
if (!calledCallback) { if (!resolvedPromise) {
RCTCallCallback(successCallback, assets, hasNextPage); RCTResolvePromise(resolve, assets, hasNextPage);
calledCallback = YES; resolvedPromise = YES;
} }
} }
} failureBlock:^(NSError *error) { } failureBlock:^(NSError *error) {
if (error.code != ALAssetsLibraryAccessUserDeniedError) { if (error.code != ALAssetsLibraryAccessUserDeniedError) {
RCTLogError(@"Failure while iterating through asset groups %@", error); RCTLogError(@"Failure while iterating through asset groups %@", error);
} }
errorCallback(error); reject(RCTErrorUnableToLoad, nil, error);
}]; }];
} }