Skip to content

Commit 630e9fa

Browse files
JoshuaGrossfacebook-github-bot
authored andcommitted
Fix permissions regression in RCTCameraRollManager
Summary: In the past (pre D13593314), ALAssetsLibrary camera operations would pop up a permissions dialogue when appropriate and block until the user responded to the dialogue. The calls that we're now using with PHPhotoLibrary immediately return if we don't have permission to access the photo library or haven't asked before, and then asynchronously pop up a permissions dialogue, causing every first photo interaction to fail. Instead we now explicitly check for permissions or request permissions before any operations. Reviewed By: PeteTheHeat Differential Revision: D13620079 fbshipit-source-id: e1befc0ddaec2c1b3334e361f5ae3a3efc5da71d
1 parent 5c0b907 commit 630e9fa

File tree

1 file changed

+130
-104
lines changed

1 file changed

+130
-104
lines changed

Libraries/CameraRoll/RCTCameraRollManager.m

+130-104
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,26 @@ @implementation RCTCameraRollManager
7676
static NSString *const kErrorUnableToSave = @"E_UNABLE_TO_SAVE";
7777
static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD";
7878

79+
static NSString *const kErrorAuthRestricted = @"E_PHOTO_LIBRARY_AUTH_RESTRICTED";
80+
static NSString *const kErrorAuthDenied = @"E_PHOTO_LIBRARY_AUTH_DENIED";
81+
82+
typedef void (^PhotosAuthorizedBlock)(void);
83+
84+
static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthorizedBlock authorizedBlock) {
85+
PHAuthorizationStatus authStatus = [PHPhotoLibrary authorizationStatus];
86+
if (authStatus == PHAuthorizationStatusRestricted) {
87+
reject(kErrorAuthRestricted, @"Access to photo library is restricted", nil);
88+
} else if (authStatus == PHAuthorizationStatusAuthorized) {
89+
authorizedBlock();
90+
} else if (authStatus == PHAuthorizationStatusNotDetermined) {
91+
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
92+
requestPhotoLibraryAccess(reject, authorizedBlock);
93+
}];
94+
} else {
95+
reject(kErrorAuthDenied, @"Access to photo library was denied", nil);
96+
}
97+
}
98+
7999
RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request
80100
type:(NSString *)type
81101
resolve:(RCTPromiseResolveBlock)resolve
@@ -115,21 +135,25 @@ @implementation RCTCameraRollManager
115135
}
116136
}];
117137
};
118-
119-
if ([type isEqualToString:@"video"]) {
120-
inputURI = request.URL;
121-
saveBlock();
122-
} else {
123-
[_bridge.imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) {
124-
if (error) {
125-
reject(kErrorUnableToLoad, nil, error);
126-
return;
127-
}
128-
129-
inputImage = image;
138+
139+
void (^loadBlock)(void) = ^void() {
140+
if ([type isEqualToString:@"video"]) {
141+
inputURI = request.URL;
130142
saveBlock();
131-
}];
132-
}
143+
} else {
144+
[self.bridge.imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) {
145+
if (error) {
146+
reject(kErrorUnableToLoad, nil, error);
147+
return;
148+
}
149+
150+
inputImage = image;
151+
saveBlock();
152+
}];
153+
}
154+
};
155+
156+
requestPhotoLibraryAccess(reject, loadBlock);
133157
}
134158

135159
static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
@@ -190,102 +214,104 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
190214
if (groupName != nil) {
191215
collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:@"localizedTitle == '%@'", groupName]];
192216
}
193-
194-
PHFetchResult<PHAssetCollection *> *const assetCollectionFetchResult = [PHAssetCollection fetchAssetCollectionsWithType:collectionType subtype:collectionSubtype options:collectionFetchOptions];
195-
[assetCollectionFetchResult enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull assetCollection, NSUInteger collectionIdx, BOOL * _Nonnull stopCollections) {
196-
// Enumerate assets within the collection
197-
PHFetchResult<PHAsset *> *const assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:assetFetchOptions];
198-
199-
[assetsFetchResult enumerateObjectsUsingBlock:^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) {
200-
NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]];
201-
if (afterCursor && !foundAfter) {
202-
if ([afterCursor isEqualToString:uri]) {
203-
foundAfter = YES;
204-
}
205-
return; // skip until we get to the first one
206-
}
207-
208-
// Get underlying resources of an asset - this includes files as well as details about edited PHAssets
209-
if ([mimeTypes count] > 0) {
210-
NSArray<PHAssetResource *> *const assetResources = [PHAssetResource assetResourcesForAsset:asset];
211-
if (![assetResources firstObject]) {
212-
return;
217+
218+
requestPhotoLibraryAccess(reject, ^{
219+
PHFetchResult<PHAssetCollection *> *const assetCollectionFetchResult = [PHAssetCollection fetchAssetCollectionsWithType:collectionType subtype:collectionSubtype options:collectionFetchOptions];
220+
[assetCollectionFetchResult enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull assetCollection, NSUInteger collectionIdx, BOOL * _Nonnull stopCollections) {
221+
// Enumerate assets within the collection
222+
PHFetchResult<PHAsset *> *const assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:assetFetchOptions];
223+
224+
[assetsFetchResult enumerateObjectsUsingBlock:^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) {
225+
NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]];
226+
if (afterCursor && !foundAfter) {
227+
if ([afterCursor isEqualToString:uri]) {
228+
foundAfter = YES;
229+
}
230+
return; // skip until we get to the first one
213231
}
214-
215-
PHAssetResource *const _Nonnull resource = [assetResources firstObject];
216-
CFStringRef const uti = (__bridge CFStringRef _Nonnull)(resource.uniformTypeIdentifier);
217-
NSString *const mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
218-
219-
BOOL __block mimeTypeFound = NO;
220-
[mimeTypes enumerateObjectsUsingBlock:^(NSString * _Nonnull mimeTypeFilter, NSUInteger idx, BOOL * _Nonnull stop) {
221-
if ([mimeType isEqualToString:mimeTypeFilter]) {
222-
mimeTypeFound = YES;
223-
*stop = YES;
232+
233+
// Get underlying resources of an asset - this includes files as well as details about edited PHAssets
234+
if ([mimeTypes count] > 0) {
235+
NSArray<PHAssetResource *> *const assetResources = [PHAssetResource assetResourcesForAsset:asset];
236+
if (![assetResources firstObject]) {
237+
return;
238+
}
239+
240+
PHAssetResource *const _Nonnull resource = [assetResources firstObject];
241+
CFStringRef const uti = (__bridge CFStringRef _Nonnull)(resource.uniformTypeIdentifier);
242+
NSString *const mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
243+
244+
BOOL __block mimeTypeFound = NO;
245+
[mimeTypes enumerateObjectsUsingBlock:^(NSString * _Nonnull mimeTypeFilter, NSUInteger idx, BOOL * _Nonnull stop) {
246+
if ([mimeType isEqualToString:mimeTypeFilter]) {
247+
mimeTypeFound = YES;
248+
*stop = YES;
249+
}
250+
}];
251+
252+
if (!mimeTypeFound) {
253+
return;
224254
}
225-
}];
226-
227-
if (!mimeTypeFound) {
228-
return;
229255
}
230-
}
231-
232-
// If we've accumulated enough results to resolve a single promise
233-
if (first == assets.count) {
234-
*stopAssets = YES;
235-
*stopCollections = YES;
236-
hasNextPage = YES;
237-
RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results.");
238-
RCTResolvePromise(resolve, assets, hasNextPage);
239-
resolvedPromise = YES;
240-
return;
241-
}
242-
243-
NSString *const assetMediaTypeLabel = (asset.mediaType == PHAssetMediaTypeVideo
244-
? @"video"
245-
: (asset.mediaType == PHAssetMediaTypeImage
246-
? @"image"
247-
: (asset.mediaType == PHAssetMediaTypeAudio
248-
? @"audio"
249-
: @"unknown")));
250-
CLLocation *const loc = asset.location;
251-
252-
// A note on isStored: in the previous code that used ALAssets, isStored
253-
// was always set to YES, probably because iCloud-synced images were never returned (?).
254-
// To get the "isStored" information and filename, we would need to actually request the
255-
// image data from the image manager. Those operations could get really expensive and
256-
// would definitely utilize the disk too much.
257-
// Thus, this field is actually not reliable.
258-
// Note that Android also does not return the `isStored` field at all.
259-
[assets addObject:@{
260-
@"node": @{
261-
@"type": assetMediaTypeLabel, // TODO: switch to mimeType?
262-
@"group_name": [assetCollection localizedTitle],
263-
@"image": @{
264-
@"uri": uri,
265-
@"height": @([asset pixelHeight]),
266-
@"width": @([asset pixelWidth]),
267-
@"isStored": @YES, // this field doesn't seem to exist on android
268-
@"playableDuration": @([asset duration]) // fractional seconds
269-
},
270-
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
271-
@"location": (loc ? @{
272-
@"latitude": @(loc.coordinate.latitude),
273-
@"longitude": @(loc.coordinate.longitude),
274-
@"altitude": @(loc.altitude),
275-
@"heading": @(loc.course),
276-
@"speed": @(loc.speed), // speed in m/s
277-
} : @{})
256+
257+
// If we've accumulated enough results to resolve a single promise
258+
if (first == assets.count) {
259+
*stopAssets = YES;
260+
*stopCollections = YES;
261+
hasNextPage = YES;
262+
RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results.");
263+
RCTResolvePromise(resolve, assets, hasNextPage);
264+
resolvedPromise = YES;
265+
return;
278266
}
267+
268+
NSString *const assetMediaTypeLabel = (asset.mediaType == PHAssetMediaTypeVideo
269+
? @"video"
270+
: (asset.mediaType == PHAssetMediaTypeImage
271+
? @"image"
272+
: (asset.mediaType == PHAssetMediaTypeAudio
273+
? @"audio"
274+
: @"unknown")));
275+
CLLocation *const loc = asset.location;
276+
277+
// A note on isStored: in the previous code that used ALAssets, isStored
278+
// was always set to YES, probably because iCloud-synced images were never returned (?).
279+
// To get the "isStored" information and filename, we would need to actually request the
280+
// image data from the image manager. Those operations could get really expensive and
281+
// would definitely utilize the disk too much.
282+
// Thus, this field is actually not reliable.
283+
// Note that Android also does not return the `isStored` field at all.
284+
[assets addObject:@{
285+
@"node": @{
286+
@"type": assetMediaTypeLabel, // TODO: switch to mimeType?
287+
@"group_name": [assetCollection localizedTitle],
288+
@"image": @{
289+
@"uri": uri,
290+
@"height": @([asset pixelHeight]),
291+
@"width": @([asset pixelWidth]),
292+
@"isStored": @YES, // this field doesn't seem to exist on android
293+
@"playableDuration": @([asset duration]) // fractional seconds
294+
},
295+
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
296+
@"location": (loc ? @{
297+
@"latitude": @(loc.coordinate.latitude),
298+
@"longitude": @(loc.coordinate.longitude),
299+
@"altitude": @(loc.altitude),
300+
@"heading": @(loc.course),
301+
@"speed": @(loc.speed), // speed in m/s
302+
} : @{})
303+
}
304+
}];
279305
}];
280306
}];
281-
}];
282-
283-
// If we get this far and haven't resolved the promise yet, we reached the end of the list of photos
284-
if (!resolvedPromise) {
285-
hasNextPage = NO;
286-
RCTResolvePromise(resolve, assets, hasNextPage);
287-
resolvedPromise = YES;
288-
}
307+
308+
// If we get this far and haven't resolved the promise yet, we reached the end of the list of photos
309+
if (!resolvedPromise) {
310+
hasNextPage = NO;
311+
RCTResolvePromise(resolve, assets, hasNextPage);
312+
resolvedPromise = YES;
313+
}
314+
});
289315
}
290316

291317
RCT_EXPORT_METHOD(deletePhotos:(NSArray<NSString *>*)assets

0 commit comments

Comments
 (0)