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 34d5fa2695
commit 9baff8f437
7 changed files with 105 additions and 95 deletions

View File

@ -128,57 +128,56 @@ class CameraRoll {
* - 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)
*
* @param successCallback Invoked with the value of `tag` on success.
* @param errorCallback Invoked with error message on error.
* Returns a Promise which when resolved will be passed the new uri
*
*/
static saveImageWithTag(tag, successCallback, errorCallback) {
static saveImageWithTag(tag) {
invariant(
typeof tag === 'string',
'CameraRoll.saveImageWithTag tag must be a valid string.'
);
RCTCameraRollManager.saveImageWithTag(
tag,
(imageTag) => {
successCallback && successCallback(imageTag);
},
(errorMessage) => {
errorCallback && errorCallback(errorMessage);
});
if (arguments.length > 1) {
console.warn("CameraRoll.saveImageWithTag(tag, success, error) is deprecated. Use the returned Promise instead");
let successCallback = arguments[1];
let errorCallback = arguments[2] || ( () => {} );
RCTCameraRollManager.saveImageWithTag(tag).then(successCallback, errorCallback);
return;
}
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`.
*
* @param {object} params See `getPhotosParamChecker`.
* @param {function} callback Invoked with arg of shape defined by
* `getPhotosReturnChecker` on success.
* @param {function} errorCallback Invoked with error message on error.
*
* Returns a Promise which when resolved will be of shape `getPhotosReturnChecker`
*
*/
static getPhotos(params, callback, errorCallback) {
var metaCallback = callback;
static getPhotos(params) {
if (__DEV__) {
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 (__DEV__) {
metaCallback = (response) => {
getPhotosReturnChecker(
{response},
'response',
'CameraRoll.getPhotos callback'
);
callback(response);
};
if (arguments.length > 1) {
console.warn("CameraRoll.getPhotos(tag, success, error) is deprecated. Use the returned Promise instead");
let successCallback = arguments[1];
if (__DEV__) {
let callback = arguments[1];
successCallback = (response) => {
getPhotosReturnChecker(
{response},
'response',
'CameraRoll.getPhotos callback'
);
callback(response);
};
}
let errorCallback = arguments[2] || ( () => {} );
RCTCameraRollManager.getPhotos(params).then(successCallback, errorCallback);
}
RCTCameraRollManager.getPhotos(params, metaCallback, errorCallback);
// TODO: Add the __DEV__ check back in to verify the Promise result
return RCTCameraRollManager.getPhotos(params);
}
}

View File

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

View File

@ -16,6 +16,7 @@
let Systrace = require('Systrace');
let ErrorUtils = require('ErrorUtils');
let JSTimersExecution = require('JSTimersExecution');
let Platform = require('Platform');
let invariant = require('invariant');
let keyMirror = require('keyMirror');
@ -318,10 +319,21 @@ class MessageQueue {
if (type === MethodTypes.remoteAsync) {
fn = function(...args) {
return new Promise((resolve, reject) => {
self.__nativeCall(module, method, args, resolve, (errorData) => {
var error = createErrorFromErrorData(errorData);
reject(error);
});
self.__nativeCall(
module,
method,
args,
(data) => {
// iOS always wraps the data in an Array regardless of what the
// shape of the data so we strip it out
// Android sends the data back properly
// TODO: Remove this once iOS has support for Promises natively (t9774697)
resolve(Platform.OS == 'ios' ? data[0] : data);
},
(errorData) => {
var error = createErrorFromErrorData(errorData);
reject(error);
});
});
};
} else {

View File

@ -12,11 +12,9 @@
'use strict';
var AndroidConstants = require('NativeModules').AndroidConstants;
var Platform = {
OS: 'android',
Version: AndroidConstants.Version,
get Version() { return require('NativeModules').AndroidConstants.Version; },
};
module.exports = Platform;

View File

@ -9,6 +9,8 @@
package com.facebook.react.bridge;
import javax.annotation.Nullable;
/**
* Interface that represents a JavaScript Promise which can be passed to the native module as a
* method parameter.
@ -22,5 +24,5 @@ public interface Promise {
@Deprecated
void reject(String reason);
void reject(String code, Throwable extra);
void reject(String code, String reason, Throwable extra);
void reject(String code, String reason, @Nullable Throwable extra);
}

View File

@ -50,7 +50,7 @@ public class PromiseImpl implements Promise {
}
@Override
public void reject(String code, String reason, Throwable extra) {
public void reject(String code, String reason, @Nullable Throwable extra) {
if (mReject != null) {
if (code == null) {
code = DEFAULT_ERROR;

View File

@ -36,8 +36,8 @@ import android.provider.MediaStore.Images;
import android.text.TextUtils;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedAsyncTask;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
@ -60,6 +60,9 @@ import com.facebook.react.common.ReactConstants;
public class CameraRollManager extends ReactContextBaseJavaModule {
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 =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
@ -112,14 +115,11 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
* by the MediaScanner.
*
* @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
* 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.)
* @param promise to be resolved or rejected
*/
@ReactMethod
public void saveImageWithTag(String uri, final Callback success, final Callback error) {
new SaveImageTag(getReactApplicationContext(), Uri.parse(uri), success, error)
public void saveImageWithTag(String uri, Promise promise) {
new SaveImageTag(getReactApplicationContext(), Uri.parse(uri), promise)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@ -127,15 +127,13 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
private final Context mContext;
private final Uri mUri;
private final Callback mSuccess;
private final Callback mError;
private final Promise mPromise;
public SaveImageTag(ReactContext context, Uri uri, Callback success, Callback error) {
public SaveImageTag(ReactContext context, Uri uri, Promise promise) {
super(context);
mContext = context;
mUri = uri;
mSuccess = success;
mError = error;
mPromise = promise;
}
@Override
@ -147,7 +145,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
pictures.mkdirs();
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;
}
File dest = new File(pictures, source.getName());
@ -178,14 +176,14 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
@Override
public void onScanCompleted(String path, Uri uri) {
if (uri != null) {
mSuccess.invoke(uri.toString());
mPromise.resolve(uri.toString());
} 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) {
mError.invoke(e.getMessage());
mPromise.reject(e);
} finally {
if (input != null && input.isOpen()) {
try {
@ -221,12 +219,11 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
* image/jpeg)
* </li>
* </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
* @param error the callback to be called on error
*/
@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");
String after = params.hasKey("after") ? params.getString("after") : null;
String groupName = params.hasKey("groupName") ? params.getString("groupName") : null;
@ -243,8 +240,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
after,
groupName,
mimeTypes,
success,
error)
promise)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@ -254,8 +250,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
private final @Nullable String mAfter;
private final @Nullable String mGroupName;
private final @Nullable ReadableArray mMimeTypes;
private final Callback mSuccess;
private final Callback mError;
private final Promise mPromise;
private GetPhotosTask(
ReactContext context,
@ -263,16 +258,14 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
@Nullable String after,
@Nullable String groupName,
@Nullable ReadableArray mimeTypes,
Callback success,
Callback error) {
Promise promise) {
super(context);
mContext = context;
mFirst = first;
mAfter = after;
mGroupName = groupName;
mMimeTypes = mimeTypes;
mSuccess = success;
mError = error;
mPromise = promise;
}
@Override
@ -309,18 +302,21 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
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 (photos == null) {
mError.invoke("Could not get photos");
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get photos", null);
} else {
try {
putEdges(resolver, photos, response, mFirst);
putPageInfo(photos, response, mFirst);
} finally {
photos.close();
mSuccess.invoke(response);
mPromise.resolve(response);
}
}
} 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);
}
}
}