@@ -76,6 +76,26 @@ @implementation RCTCameraRollManager
76
76
static NSString *const kErrorUnableToSave = @" E_UNABLE_TO_SAVE" ;
77
77
static NSString *const kErrorUnableToLoad = @" E_UNABLE_TO_LOAD" ;
78
78
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
+
79
99
RCT_EXPORT_METHOD (saveToCameraRoll:(NSURLRequest *)request
80
100
type:(NSString *)type
81
101
resolve:(RCTPromiseResolveBlock)resolve
@@ -115,21 +135,25 @@ @implementation RCTCameraRollManager
115
135
}
116
136
}];
117
137
};
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 ;
130
142
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);
133
157
}
134
158
135
159
static void RCTResolvePromise (RCTPromiseResolveBlock resolve,
@@ -190,102 +214,104 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
190
214
if (groupName != nil ) {
191
215
collectionFetchOptions.predicate = [NSPredicate predicateWithFormat: [NSString stringWithFormat: @" localizedTitle == '%@ '" , groupName]];
192
216
}
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
213
231
}
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 ;
224
254
}
225
- }];
226
-
227
- if (!mimeTypeFound) {
228
- return ;
229
255
}
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 ;
278
266
}
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
+ }];
279
305
}];
280
306
}];
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
+ });
289
315
}
290
316
291
317
RCT_EXPORT_METHOD (deletePhotos:(NSArray <NSString *>*)assets
0 commit comments