This is an automated email from the ASF dual-hosted git repository.

manuelbeck pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-plugin-camera.git


The following commit(s) were added to refs/heads/master by this push:
     new d731cb9  fix(ios): don't copy temporary video file on main thread and 
cleanup (#942)
d731cb9 is described below

commit d731cb9ad30207f8b5362c84b6aea4e4506bf1f1
Author: Manuel Beck <[email protected]>
AuthorDate: Thu Jan 29 14:08:55 2026 +0100

    fix(ios): don't copy temporary video file on main thread and cleanup (#942)
    
    - A user commented that`window.resolveLocalFileSystemURL` will fail when 
using the uri result for picking a video with PHPicker: 
https://github.com/apache/cordova-plugin-camera/issues/935#issuecomment-3742776758.
 The temporary video file provided by PHPickerViewController gets deleted when 
the completion handler exits. The copying of the video file was wrongly done on 
the main thread, rather than on the completion handler background thread.
    - Report error to webview, if the video file could not successful copied to 
the temp directory
    
    * Code refactoring and documentation
    
    - Renamed `createTmpVideo` to `copyFileToTemp`, since it has no special 
code for just video files
    - Log errors, if something is wrong in `copyFileToTemp `
    - Document property `hasPendingOperation`
    - Remove `dispatch_async` for returning `CDVPluginResult`. Internally, 
`CDVCommandDelegate` has a queue of plugin messages to send and it ensures that 
those are always sent to the webview on the main thread.
    - Only use `dispatch_async(dispatch_get_main_queue(), ...` for UI operations
    - Some minor code formatting for calling `CDVPluginResult` initializer
    - Removed method `urlTransformer` which was deprecated in cordova-ios 8 and 
has no use anymore
    - Removed unused method `integerValueForKey`: This method is legacy and was 
nowhere used
    - Resolve method `processPHPickerImage`. This extra method is not needed
    - Replace `IsAtLeastiOSVersion` with `@available`, `IsAtLeastiOSVersion` is 
deprecated
    - Remove `IsAtLeastiOSVersion(@"8.0")` check for [locationManager 
requestWhenInUseAuthorization]. This plugins supports minimum iOS 11.
    - Documentation and small refactoring regarding gettting GPS location for 
capturing JPEGs
    - Use modern code for working with dictionaries
    - fix: deprecation of `requestImageDataForAsset:options:resultHandler:`, 
which was deprecated in iOS 13. Replaced with 
`requestImageDataAndOrientationForAsset:options:resultHandler:`, which was 
introduced in iOS 11.
    
    * Some cleanup regarding temporary file handling
    
    - Renamed `createTmpVideo`to `copyFileToTemp` and keep the method generic, 
since it has no special code for video files
    - Renamed `tempFilePath` to `tempFilePathForExtension`, since it creates 
unique path only for a file extension
    - Document `tempFilePathForExtension`
    - Refactoring and documentation of method `cleanup:`
    - Moved `cleanup`, `tempFilePath` and `copyFileToTemp` at one place, since 
they are related
    - creating unique temporary files with milliseconds: This is more precise 
than just using seconds
---
 src/ios/CDVCamera.h |   5 +-
 src/ios/CDVCamera.m | 594 ++++++++++++++++++++++++++++++----------------------
 2 files changed, 342 insertions(+), 257 deletions(-)

diff --git a/src/ios/CDVCamera.h b/src/ios/CDVCamera.h
index 37f4fcf..8f57060 100644
--- a/src/ios/CDVCamera.h
+++ b/src/ios/CDVCamera.h
@@ -74,6 +74,10 @@ typedef NSUInteger CDVMediaType;
 @property (assign) BOOL saveToPhotoAlbum;
 @property (assign) UIImagePickerControllerCameraDevice cameraDirection;
 
+/** 
+  Include GPS location information in the image's EXIF metadata, when 
capturing JPEGs.
+  This is YES when the preference `CameraUsesGeolocation` is set to true in 
config.xml.
+*/
 @property (assign) BOOL usesGeolocation;
 @property (assign) BOOL cropToSize;
 
@@ -133,7 +137,6 @@ typedef NSUInteger CDVMediaType;
 // PHPickerViewController specific methods (iOS 14+)
 #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 // Always true on XCode12+
 - (void)showPHPicker:(NSString*)callbackId 
withOptions:(CDVPictureOptions*)pictureOptions API_AVAILABLE(ios(14));
-- (void)processPHPickerImage:(UIImage*)image 
assetIdentifier:(NSString*)assetIdentifier callbackId:(NSString*)callbackId 
options:(CDVPictureOptions*)options API_AVAILABLE(ios(14));
 - (void)finalizePHPickerImage:(UIImage*)image metadata:(NSDictionary*)metadata 
callbackId:(NSString*)callbackId options:(CDVPictureOptions*)options 
API_AVAILABLE(ios(14));
 // PHPickerViewControllerDelegate method
 - (void)picker:(PHPickerViewController *)picker 
didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14));
diff --git a/src/ios/CDVCamera.m b/src/ios/CDVCamera.m
index 0284351..c8a893f 100644
--- a/src/ios/CDVCamera.m
+++ b/src/ios/CDVCamera.m
@@ -94,6 +94,9 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 
 @interface CDVCamera ()
 
+// Redeclare CDVPlugin.hasPendingOperation as readwrite,
+// so we can set it. Skips didReceiveMemoryWarning handling in
+// CDVViewController when a plugin has a pending operation.
 @property (readwrite, assign) BOOL hasPendingOperation;
 
 @end
@@ -102,24 +105,12 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 
 @synthesize hasPendingOperation, pickerController, locationManager;
 
-- (NSURL*)urlTransformer:(NSURL*)url
-{
-    NSURL* urlToTransform = url;
-
-    // for backwards compatibility - we check if this property is there
-    SEL sel = NSSelectorFromString(@"urlTransformer");
-    if ([self.commandDelegate respondsToSelector:sel]) {
-        // grab the block from the commandDelegate
-        NSURL* (^urlTransformer)(NSURL*) = ((id(*)(id, 
SEL))objc_msgSend)(self.commandDelegate, sel);
-        // if block is not null, we call it
-        if (urlTransformer) {
-            urlToTransform = urlTransformer(url);
-        }
-    }
-
-    return urlToTransform;
-}
+/**
+    Reads the preference CameraUsesGeolocation from config.xml
+    to determine whether to include GPS location data in JPEG EXIF metadata.
 
+    @return YES if CameraUsesGeolocation is set to true, NO otherwise.
+*/
 - (BOOL)usesGeolocation
 {
     id useGeo = [self.commandDelegate.settings 
objectForKey:[@"CameraUsesGeolocation" lowercaseString]];
@@ -155,6 +146,7 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 
     [self.commandDelegate runInBackground:^{
         CDVPictureOptions* pictureOptions = [CDVPictureOptions 
createFromTakePictureArguments:command];
+        // Only for capturing JPEG images, get geolocation data to include in 
the EXIF header
         pictureOptions.usesGeolocation = [weakSelf usesGeolocation];
         pictureOptions.cropToSize = NO;
 
@@ -210,24 +202,23 @@ static NSString* MIME_JPEG    = @"image/jpeg";
  */
 - (void)presentPermissionDeniedAlertWithMessage:(NSString*)message 
callbackId:(NSString*)callbackId
 {
+    __weak CDVCamera *weakSelf = self;
+
+    // Perform UI creation and presentation on the main thread
     dispatch_async(dispatch_get_main_queue(), ^{
-        
         NSString *bundleDisplayName = [[NSBundle mainBundle] 
objectForInfoDictionaryKey:@"CFBundleDisplayName"];
         UIAlertController *alertController = [UIAlertController 
alertControllerWithTitle:bundleDisplayName
                                                                                
  message:NSLocalizedString(message, nil)
                                                                           
preferredStyle:UIAlertControllerStyleAlert];
-        
-        // Add buttons
-        __weak CDVCamera *weakSelf = self;
-        
+
         // Ok button
         [alertController addAction:[UIAlertAction 
actionWithTitle:NSLocalizedString(@"OK", nil)
                                                             
style:UIAlertActionStyleDefault
                                                           
handler:^(UIAlertAction * _Nonnull action) {
             [weakSelf sendNoPermissionResult:callbackId];
         }]];
-        
-        // Button for open settings
+
+         // Button for open settings
         [alertController addAction:[UIAlertAction 
actionWithTitle:NSLocalizedString(@"Settings", nil)
                                                             
style:UIAlertActionStyleDefault
                                                           
handler:^(UIAlertAction * _Nonnull action) {
@@ -270,20 +261,20 @@ static NSString* MIME_JPEG    = @"image/jpeg";
  */
 - (void)showCameraPicker:(NSString*)callbackId 
withOptions:(CDVPictureOptions*)pictureOptions
 {
-    // Perform UI operations on the main thread
-    dispatch_async(dispatch_get_main_queue(), ^{
-        // Use PHPickerViewController for photo library on iOS 14+
-        if (@available(iOS 14, *)) {
-            // sourceType is PHOTOLIBRARY
-            if (pictureOptions.sourceType == 
UIImagePickerControllerSourceTypePhotoLibrary ||
-                // sourceType is SAVEDPHOTOALBUM (same as PHOTOLIBRARY)
-                pictureOptions.sourceType == 
UIImagePickerControllerSourceTypeSavedPhotosAlbum) {
-                [self showPHPicker:callbackId withOptions:pictureOptions];
-                return;
-            }
+    // Use PHPickerViewController for photo library on iOS 14+
+    if (@available(iOS 14, *)) {
+        // sourceType is PHOTOLIBRARY
+        if (pictureOptions.sourceType == 
UIImagePickerControllerSourceTypePhotoLibrary ||
+            // sourceType is SAVEDPHOTOALBUM (same as PHOTOLIBRARY)
+            pictureOptions.sourceType == 
UIImagePickerControllerSourceTypeSavedPhotosAlbum) {
+            [self showPHPicker:callbackId withOptions:pictureOptions];
+            return;
         }
-        
-        // Use UIImagePickerController for camera or as image picker for iOS 
older than 14
+    }
+
+    // Use UIImagePickerController for camera or as image picker for iOS older 
than 14
+    // UIImagePickerController must be created and presented on the main 
thread.
+    dispatch_async(dispatch_get_main_queue(), ^{
         CDVCameraPicker* cameraPicker = [CDVCameraPicker 
createFromPictureOptions:pictureOptions];
         self.pickerController = cameraPicker;
 
@@ -292,6 +283,7 @@ static NSString* MIME_JPEG    = @"image/jpeg";
         // we need to capture this state for memory warnings that dealloc this 
object
         cameraPicker.webView = self.webView;
         cameraPicker.modalPresentationStyle = 
UIModalPresentationCurrentContext;
+
         [self.viewController presentViewController:cameraPicker
                                           animated:YES
                                         completion:^{
@@ -304,41 +296,44 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 // Always true on XCode12+
 - (void)showPHPicker:(NSString*)callbackId 
withOptions:(CDVPictureOptions*)pictureOptions API_AVAILABLE(ios(14))
 {
-    PHPickerConfiguration *config = [[PHPickerConfiguration alloc] init];
-    
-    // Configure filter based on media type
-    // Images
-    if (pictureOptions.mediaType == MediaTypePicture) {
-        config.filter = [PHPickerFilter imagesFilter];
-        
-        // Videos
-    } else if (pictureOptions.mediaType == MediaTypeVideo) {
-        config.filter = [PHPickerFilter videosFilter];
-        
-        // Images and videos
-    } else if (pictureOptions.mediaType == MediaTypeAll) {
-        config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[
-            [PHPickerFilter imagesFilter],
-            [PHPickerFilter videosFilter]
-        ]];
-    }
-    
-    config.selectionLimit = 1;
-    config.preferredAssetRepresentationMode = 
PHPickerConfigurationAssetRepresentationModeCurrent;
-    
-    PHPickerViewController *picker = [[PHPickerViewController alloc] 
initWithConfiguration:config];
-    picker.delegate = self;
-    
-    // Store callback ID and options in picker with objc_setAssociatedObject
-    // PHPickerViewController’s delegate method picker:didFinishPicking: only 
gives you back the picker instance
-    // and the results array. It doesn’t carry arbitrary context. By 
associating the callbackId and pictureOptions
-    // with the picker, you can retrieve them later inside the delegate method
-    objc_setAssociatedObject(picker, "callbackId", callbackId, 
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    objc_setAssociatedObject(picker, "pictureOptions", pictureOptions, 
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    
-    [self.viewController presentViewController:picker animated:YES 
completion:^{
-        self.hasPendingOperation = NO;
-    }];
+    // PHPicker must be created and presented on the main thread.
+    dispatch_async(dispatch_get_main_queue(), ^{
+        PHPickerConfiguration *config = [[PHPickerConfiguration alloc] init];
+
+        // Configure filter based on media type
+        // Images
+        if (pictureOptions.mediaType == MediaTypePicture) {
+            config.filter = [PHPickerFilter imagesFilter];
+
+            // Videos
+        } else if (pictureOptions.mediaType == MediaTypeVideo) {
+            config.filter = [PHPickerFilter videosFilter];
+
+            // Images and videos
+        } else if (pictureOptions.mediaType == MediaTypeAll) {
+            config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[
+                [PHPickerFilter imagesFilter],
+                [PHPickerFilter videosFilter]
+            ]];
+        }
+
+        config.selectionLimit = 1;
+        config.preferredAssetRepresentationMode = 
PHPickerConfigurationAssetRepresentationModeCurrent;
+
+        PHPickerViewController *picker = [[PHPickerViewController alloc] 
initWithConfiguration:config];
+        picker.delegate = self;
+
+        // Store callback ID and options in picker with 
objc_setAssociatedObject
+        // PHPickerViewController’s delegate method picker:didFinishPicking: 
only gives you back the picker instance
+        // and the results array. It doesn’t carry arbitrary context. By 
associating the callbackId and pictureOptions
+        // with the picker, you can retrieve them later inside the delegate 
method
+        objc_setAssociatedObject(picker, "callbackId", callbackId, 
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+        objc_setAssociatedObject(picker, "pictureOptions", pictureOptions, 
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+
+        [self.viewController presentViewController:picker animated:YES 
completion:^{
+            self.hasPendingOperation = NO;
+        }];
+    });
 }
 
 // PHPickerViewControllerDelegate method
@@ -362,20 +357,34 @@ static NSString* MIME_JPEG    = @"image/jpeg";
         
         // Check if it's a video
         if ([pickerResult.itemProvider 
hasItemConformingToTypeIdentifier:UTTypeMovie.identifier]) {
-            [pickerResult.itemProvider 
loadFileRepresentationForTypeIdentifier:UTTypeMovie.identifier 
completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) {
+            // loadFileRepresentationForTypeIdentifier returns an url which 
will be gone after the completion handler returns,
+            // so we need to copy the video to a temporary location, which can 
be accessed later
+            [pickerResult.itemProvider 
loadFileRepresentationForTypeIdentifier:UTTypeMovie.identifier
+                                                             
completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) {
                 if (error) {
-                    CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error 
localizedDescription]];
+                    NSLog(@"CDVCamera: Failed to load video: %@", [error 
localizedDescription]);
+                    CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_IO_EXCEPTION
+                                                                
messageAsString:[NSString stringWithFormat:@"Failed to load video: %@", [error 
localizedDescription]]];
                     [weakSelf.commandDelegate sendPluginResult:result 
callbackId:callbackId];
                     weakSelf.hasPendingOperation = NO;
                     return;
                 }
                 
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    NSString* videoPath = [weakSelf createTmpVideo:[url path]];
-                    CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_OK messageAsString:videoPath];
-                    [weakSelf.commandDelegate sendPluginResult:result 
callbackId:callbackId];
-                    weakSelf.hasPendingOperation = NO;
-                });
+                // Copy video to a temporary location, so it can be accessed 
after this completion handler returns
+                NSString* tempVideoPath = [weakSelf copyFileToTemp:[url path]];
+                
+                // Send Cordova plugin result back
+                CDVPluginResult* result = nil;
+                
+                if (tempVideoPath == nil) {
+                    result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_IO_EXCEPTION
+                                                messageAsString:@"Failed to 
copy video file to temporary location"];
+                } else {
+                    result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_OK messageAsString:tempVideoPath];
+                }
+                
+                [weakSelf.commandDelegate sendPluginResult:result 
callbackId:callbackId];
+                weakSelf.hasPendingOperation = NO;
             }];
             
             // Handle image
@@ -389,51 +398,38 @@ static NSString* MIME_JPEG    = @"image/jpeg";
                 }
                 
                 UIImage *image = (UIImage *)object;
+                    
+                // Fetch metadata if asset identifier is available
+                if (pickerResult.assetIdentifier) {
+                    PHFetchResult *result = [PHAsset 
fetchAssetsWithLocalIdentifiers:@[pickerResult.assetIdentifier] options:nil];
+                    PHAsset *asset = result.firstObject;
+                    
+                    if (asset) {
+                        PHImageRequestOptions *imageOptions = 
[[PHImageRequestOptions alloc] init];
+                        imageOptions.synchronous = YES;
+                        imageOptions.networkAccessAllowed = YES;
+                        
+                        [[PHImageManager defaultManager] 
requestImageDataAndOrientationForAsset:asset
+                                                                               
         options:imageOptions
+                                                                               
   resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, 
CGImagePropertyOrientation orientation, NSDictionary *_Nullable info) {
+                            NSDictionary *metadata = imageData ? [weakSelf 
convertImageMetadata:imageData] : nil;
+                            [weakSelf finalizePHPickerImage:image
+                                                   metadata:metadata
+                                                 callbackId:callbackId
+                                                    options:pictureOptions];
+                        }];
+
+                        return;
+                    }
+                }
                 
-                // Get asset identifier to fetch metadata
-                NSString *assetIdentifier = pickerResult.assetIdentifier;
-                
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    [weakSelf processPHPickerImage:image 
assetIdentifier:assetIdentifier callbackId:callbackId options:pictureOptions];
-                });
+                // No metadata available
+                [self finalizePHPickerImage:image metadata:nil 
callbackId:callbackId options:pictureOptions];
             }];
         }
     }];
 }
 
-- (void)processPHPickerImage:(UIImage*)image
-             assetIdentifier:(NSString*)assetIdentifier
-                  callbackId:(NSString*)callbackId
-                     options:(CDVPictureOptions*)options API_AVAILABLE(ios(14))
-{
-    __weak CDVCamera* weakSelf = self;
-    
-    // Fetch metadata if asset identifier is available
-    if (assetIdentifier) {
-        PHFetchResult *result = [PHAsset 
fetchAssetsWithLocalIdentifiers:@[assetIdentifier] options:nil];
-        PHAsset *asset = result.firstObject;
-        
-        if (asset) {
-            PHImageRequestOptions *imageOptions = [[PHImageRequestOptions 
alloc] init];
-            imageOptions.synchronous = YES;
-            imageOptions.networkAccessAllowed = YES;
-            
-            [[PHImageManager defaultManager] 
requestImageDataAndOrientationForAsset:asset
-                                                                            
options:imageOptions
-                                                                      
resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, 
CGImagePropertyOrientation orientation, NSDictionary *_Nullable info) {
-                NSDictionary *metadata = imageData ? [weakSelf 
convertImageMetadata:imageData] : nil;
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    [weakSelf finalizePHPickerImage:image metadata:metadata 
callbackId:callbackId options:options];
-                });
-            }];
-            return;
-        }
-    }
-    
-    // No metadata available
-    [self finalizePHPickerImage:image metadata:nil callbackId:callbackId 
options:options];
-}
-
 - (void)finalizePHPickerImage:(UIImage*)image
                      metadata:(NSDictionary*)metadata
                    callbackId:(NSString*)callbackId
@@ -446,64 +442,58 @@ static NSString* MIME_JPEG    = @"image/jpeg";
         processedImage = [processedImage imageCorrectedForCaptureOrientation];
     }
     
+    // Scale with optional cropping
     if ((options.targetSize.width > 0) && (options.targetSize.height > 0)) {
+        // Scale and crop to target size
         if (options.cropToSize) {
             processedImage = [processedImage 
imageByScalingAndCroppingForSize:options.targetSize];
+
+            // Scale with no cropping
         } else {
             processedImage = [processedImage 
imageByScalingNotCroppingForSize:options.targetSize];
         }
     }
-    
-    // Create info dictionary similar to UIImagePickerController
-    NSMutableDictionary *info = [NSMutableDictionary dictionary];
-    [info setObject:processedImage 
forKey:UIImagePickerControllerOriginalImage];
-    if (metadata) {
-        [info setObject:metadata 
forKey:@"UIImagePickerControllerMediaMetadata"];
-    }
-    
-    // Store metadata for processing
-    if (metadata) {
-        self.metadata = [[NSMutableDictionary alloc] init];
-        
-        NSMutableDictionary* EXIFDictionary = [[metadata 
objectForKey:(NSString*)kCGImagePropertyExifDictionary] mutableCopy];
-        if (EXIFDictionary) {
-            [self.metadata setObject:EXIFDictionary 
forKey:(NSString*)kCGImagePropertyExifDictionary];
+
+    // Store metadata, which will be processed in resultForImage
+    if (metadata.count > 0) {
+        self.metadata = [NSMutableDictionary dictionary];
+
+        NSDictionary *exif = metadata[(NSString 
*)kCGImagePropertyExifDictionary];
+        if (exif.count > 0) {
+            self.metadata[(NSString *)kCGImagePropertyExifDictionary] = [exif 
mutableCopy];
         }
-        
-        NSMutableDictionary* TIFFDictionary = [[metadata 
objectForKey:(NSString*)kCGImagePropertyTIFFDictionary] mutableCopy];
-        if (TIFFDictionary) {
-            [self.metadata setObject:TIFFDictionary 
forKey:(NSString*)kCGImagePropertyTIFFDictionary];
+
+        NSDictionary *tiff = metadata[(NSString 
*)kCGImagePropertyTIFFDictionary];
+        if (tiff.count > 0) {
+            self.metadata[(NSString *)kCGImagePropertyTIFFDictionary] = [tiff 
mutableCopy];
         }
-        
-        NSMutableDictionary* GPSDictionary = [[metadata 
objectForKey:(NSString*)kCGImagePropertyGPSDictionary] mutableCopy];
-        if (GPSDictionary) {
-            [self.metadata setObject:GPSDictionary 
forKey:(NSString*)kCGImagePropertyGPSDictionary];
+
+        NSDictionary *gps = metadata[(NSString 
*)kCGImagePropertyGPSDictionary];
+        if (gps.count > 0) {
+            self.metadata[(NSString *)kCGImagePropertyGPSDictionary] = [gps 
mutableCopy];
         }
     }
     
+    // Return Cordova result to WebView
+    // Needed weakSelf for completion block
     __weak CDVCamera* weakSelf = self;
     
+    // Create info dictionary similar to UIImagePickerController
+    NSMutableDictionary *info = [@{ UIImagePickerControllerOriginalImage : 
processedImage } mutableCopy];
+    
+    if (metadata.count > 0) {
+        info[UIImagePickerControllerMediaMetadata] = metadata;
+    }
+    
     // Process and return result
-    [self resultForImage:options info:info completion:^(CDVPluginResult* res) {
-        [weakSelf.commandDelegate sendPluginResult:res callbackId:callbackId];
+    [self resultForImage:options info:info completion:^(CDVPluginResult* 
pluginResult) {
+        [weakSelf.commandDelegate sendPluginResult:pluginResult 
callbackId:callbackId];
         weakSelf.hasPendingOperation = NO;
         weakSelf.pickerController = nil;
     }];
 }
 #endif
 
-- (NSInteger)integerValueForKey:(NSDictionary*)dict key:(NSString*)key 
defaultValue:(NSInteger)defaultValue
-{
-    NSInteger value = defaultValue;
-
-    NSNumber* val = [dict valueForKey:key];  // value is an NSNumber
-
-    if (val != nil) {
-        value = [val integerValue];
-    }
-    return value;
-}
-
 // UINavigationControllerDelegate method
 - (void)navigationController:(UINavigationController*)navigationController
       willShowViewController:(UIViewController*)viewController
@@ -521,41 +511,6 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     }
 }
 
-- (void)cleanup:(CDVInvokedUrlCommand*)command
-{
-    // empty the tmp directory
-    NSFileManager* fileMgr = [[NSFileManager alloc] init];
-    NSError* err = nil;
-    BOOL hasErrors = NO;
-
-    // clear contents of NSTemporaryDirectory
-    NSString* tempDirectoryPath = NSTemporaryDirectory();
-    NSDirectoryEnumerator* directoryEnumerator = [fileMgr 
enumeratorAtPath:tempDirectoryPath];
-    NSString* fileName = nil;
-    BOOL result;
-
-    while ((fileName = [directoryEnumerator nextObject])) {
-        // only delete the files we created
-        if (![fileName hasPrefix:CDV_PHOTO_PREFIX]) {
-            continue;
-        }
-        NSString* filePath = [tempDirectoryPath 
stringByAppendingPathComponent:fileName];
-        result = [fileMgr removeItemAtPath:filePath error:&err];
-        if (!result && err) {
-            NSLog(@"Failed to delete: %@ (error: %@)", filePath, err);
-            hasErrors = YES;
-        }
-    }
-
-    CDVPluginResult* pluginResult;
-    if (hasErrors) {
-        pluginResult = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:@"One or more 
files failed to be deleted."];
-    } else {
-        pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
-    }
-    [self.commandDelegate sendPluginResult:pluginResult 
callbackId:command.callbackId];
-}
-
 - (NSString*)getMimeForEncoding:(CDVEncodingType)encoding
 {
     switch (encoding) {
@@ -602,6 +557,7 @@ static NSString* MIME_JPEG    = @"image/jpeg";
         case EncodingTypeJPEG:
         {
             if (outMime != nil) *outMime = MIME_JPEG;
+
             if ((options.allowsEditing == NO) && (options.targetSize.width <= 
0) && (options.targetSize.height <= 0) && (options.correctOrientation == NO) && 
(([options.quality integerValue] == 100) || (options.sourceType != 
UIImagePickerControllerSourceTypeCamera))){
                 // use image unedited as requested , don't resize
                 data = UIImageJPEGRepresentation(image, 1.0);
@@ -610,46 +566,55 @@ static NSString* MIME_JPEG    = @"image/jpeg";
             }
 
             if (pickerController.sourceType == 
UIImagePickerControllerSourceTypeCamera) {
+                // Include geolocation data in EXIF metadata if requested, 
this will
+                // be done in locationManager:didUpdateLocations:
+                // Note: This will be done only if 
UIImagePickerControllerMediaMetadata is available
                 if (options.usesGeolocation) {
-                    NSDictionary* controllerMetadata = [info 
objectForKey:@"UIImagePickerControllerMediaMetadata"];
-                    if (controllerMetadata) {
+
+                    // Get the metadata from the UIImagePickerController info 
dictionary
+                    NSDictionary *mediaMetadata = 
info[UIImagePickerControllerMediaMetadata];
+                    
+                    // Get location if mediaMetadata is set
+                    if (mediaMetadata) {
                         self.data = data;
                         self.metadata = [[NSMutableDictionary alloc] init];
+                        
+                        NSDictionary *exifDict = mediaMetadata[(NSString 
*)kCGImagePropertyExifDictionary];
 
-                        NSMutableDictionary* EXIFDictionary = 
[[controllerMetadata 
objectForKey:(NSString*)kCGImagePropertyExifDictionary]mutableCopy];
-                        if (EXIFDictionary)    {
-                            [self.metadata setObject:EXIFDictionary 
forKey:(NSString*)kCGImagePropertyExifDictionary];
+                        if (exifDict.count > 0) {
+                            self.metadata[(NSString 
*)kCGImagePropertyExifDictionary] = [exifDict mutableCopy];
                         }
 
-                        if (IsAtLeastiOSVersion(@"8.0")) {
-                            [[self locationManager] 
performSelector:NSSelectorFromString(@"requestWhenInUseAuthorization") 
withObject:nil afterDelay:0];
-                        }
+                        [[self locationManager] requestWhenInUseAuthorization];
                         [[self locationManager] startUpdatingLocation];
                     }
-                data = nil;
+
+                    // Don't return anything if options.usesGeolocation is set
+                    // Data will be returned in 
locationManager:didUpdateLocations: or locationManager:didFailWithError:
+                    // Note: If mediaMetadata is not set, this would also be 
set to nil, is this expected?
+                    data = nil;
                 }
             } else if (pickerController.sourceType == 
UIImagePickerControllerSourceTypePhotoLibrary) {
                 PHAsset* asset = [info 
objectForKey:@"UIImagePickerControllerPHAsset"];
                 NSDictionary* controllerMetadata = [self 
getImageMetadataFromAsset:asset];
-
                 self.data = data;
-                if (controllerMetadata) {
-                    self.metadata = [[NSMutableDictionary alloc] init];
 
-                    NSMutableDictionary* EXIFDictionary = [[controllerMetadata 
objectForKey:(NSString*)kCGImagePropertyExifDictionary]mutableCopy];
-                    if (EXIFDictionary)    {
-                        [self.metadata setObject:EXIFDictionary 
forKey:(NSString*)kCGImagePropertyExifDictionary];
+                if (controllerMetadata.count > 0) {
+                    self.metadata = [NSMutableDictionary dictionary];
+
+                    NSDictionary *exif = controllerMetadata[(NSString 
*)kCGImagePropertyExifDictionary];
+                    if (exif.count > 0) {
+                        self.metadata[(NSString 
*)kCGImagePropertyExifDictionary] = [exif mutableCopy];
                     }
-                    NSMutableDictionary* TIFFDictionary = [[controllerMetadata 
objectForKey:(NSString*)kCGImagePropertyTIFFDictionary
-                    ]mutableCopy];
-                    if (TIFFDictionary)    {
-                        [self.metadata setObject:TIFFDictionary 
forKey:(NSString*)kCGImagePropertyTIFFDictionary];
+
+                    NSDictionary *tiff = controllerMetadata[(NSString 
*)kCGImagePropertyTIFFDictionary];
+                    if (tiff.count > 0) {
+                        self.metadata[(NSString 
*)kCGImagePropertyTIFFDictionary] = [tiff mutableCopy];
                     }
-                    NSMutableDictionary* GPSDictionary = [[controllerMetadata 
objectForKey:(NSString*)kCGImagePropertyGPSDictionary
-]mutableCopy];
-                    if (GPSDictionary)    {
-                        [self.metadata setObject:GPSDictionary 
forKey:(NSString*)kCGImagePropertyGPSDictionary
-];
+
+                    NSDictionary *gps = controllerMetadata[(NSString 
*)kCGImagePropertyGPSDictionary];
+                    if (gps.count > 0) {
+                        self.metadata[(NSString 
*)kCGImagePropertyGPSDictionary] = [gps mutableCopy];
                     }
                 }
             }
@@ -669,29 +634,31 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 -------------------------------------------------------------- */
 - (NSDictionary*)getImageMetadataFromAsset:(PHAsset*)asset
 {
-    if(asset == nil) {
-        return nil;
-    }
+    if(asset == nil) return nil;
 
     // get photo info from this asset
     __block NSDictionary *dict = nil;
     PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions 
alloc] init];
     imageRequestOptions.synchronous = YES;
-    [[PHImageManager defaultManager]
-     requestImageDataForAsset:asset
-     options:imageRequestOptions
-     resultHandler: ^(NSData *imageData, NSString *dataUTI, UIImageOrientation 
orientation, NSDictionary *info) {
-        dict = [self convertImageMetadata:imageData]; // as this imageData is 
in NSData format so we need a method to convert this NSData into NSDictionary
-     }];
+    
+    [[PHImageManager defaultManager] 
requestImageDataAndOrientationForAsset:asset
+                                                                    
options:imageRequestOptions
+                                                                
resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, 
CGImagePropertyOrientation orientation, NSDictionary *_Nullable info) {
+        // as this imageData is in NSData format so we need a method to 
convert this NSData into NSDictionary
+        dict = [self convertImageMetadata:imageData];
+    }];
+    
     return dict;
 }
 
 - (NSDictionary*)convertImageMetadata:(NSData*)imageData
 {
     CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge 
CFDataRef)(imageData), NULL);
+
     if (imageSource) {
         NSDictionary *options = @{(NSString *)kCGImageSourceShouldCache : 
[NSNumber numberWithBool:NO]};
         CFDictionaryRef imageProperties = 
CGImageSourceCopyPropertiesAtIndex(imageSource, 0, (__bridge 
CFDictionaryRef)options);
+
         if (imageProperties) {
             NSDictionary *metadata = (__bridge NSDictionary *)imageProperties;
             CFRelease(imageProperties);
@@ -699,6 +666,7 @@ static NSString* MIME_JPEG    = @"image/jpeg";
             NSLog(@"Metadata of selected image%@", metadata);// image metadata 
after converting NSData into NSDictionary
             return metadata;
         }
+
         CFRelease(imageSource);
     }
 
@@ -717,7 +685,7 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 - (void)options:(CDVPictureOptions*)options requestPhotoPermissions:(void 
(^)(BOOL auth))completion
 {
     // This is would be no good response
-    if(options.sourceType == UIImagePickerControllerSourceTypeCamera) {
+    if (options.sourceType == UIImagePickerControllerSourceTypeCamera) {
         completion(YES);
     } else {
         PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
@@ -745,17 +713,6 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 
 }
 
-- (NSString*)tempFilePath:(NSString*)extension
-{
-    NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath];
-    // unique file name
-    NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970];
-    NSNumber *timeStampObj = [NSNumber numberWithDouble: timeStamp];
-    NSString* filePath = [NSString stringWithFormat:@"%@/%@%ld.%@", docsPath, 
CDV_PHOTO_PREFIX, [timeStampObj longValue], extension];
-
-    return filePath;
-}
-
 - (UIImage*)retrieveImage:(NSDictionary*)info 
options:(CDVPictureOptions*)options
 {
     // get the image
@@ -826,27 +783,31 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 
                     NSError* err = nil;
                     NSString* extension = 
self.pickerController.pictureOptions.encodingType == EncodingTypePNG ? 
@"png":@"jpg";
-                    NSString* filePath = [self tempFilePath:extension];
+                    NSString* filePath = [self 
tempFilePathForExtension:extension];
 
                     // save file
                     if (![imageDataWithExif writeToFile:filePath 
options:NSAtomicWrite error:&err]) {
-                        result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[err 
localizedDescription]];
+                        result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_IO_EXCEPTION
+                                                   messageAsString:[err 
localizedDescription]];
                     }
                     else {
-                        result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_OK messageAsString:[[self 
urlTransformer:[NSURL fileURLWithPath:filePath]] absoluteString]];
+                        result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_OK
+                                                   messageAsString:[[NSURL 
fileURLWithPath:filePath] absoluteString]];
                     }
                     
                 } else if (pickerController.sourceType != 
UIImagePickerControllerSourceTypeCamera || !options.usesGeolocation) {
                     // No need to save file if usesGeolocation is true since 
it will be saved after the location is tracked
                     NSString* extension = options.encodingType == 
EncodingTypePNG? @"png" : @"jpg";
-                    NSString* filePath = [self tempFilePath:extension];
+                    NSString* filePath = [self 
tempFilePathForExtension:extension];
                     NSError* err = nil;
 
                     // save file
                     if (![data writeToFile:filePath options:NSAtomicWrite 
error:&err]) {
-                        result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[err 
localizedDescription]];
+                        result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_IO_EXCEPTION
+                                                   messageAsString:[err 
localizedDescription]];
                     } else {
-                        result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_OK messageAsString:[[self 
urlTransformer:[NSURL fileURLWithPath:filePath]] absoluteString]];
+                        result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_OK
+                                                   messageAsString:[[NSURL 
fileURLWithPath:filePath] absoluteString]];
                     }
                 }
 
@@ -865,21 +826,113 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 - (CDVPluginResult*)resultForVideo:(NSDictionary*)info
 {
     NSString* moviePath = [[info objectForKey:UIImagePickerControllerMediaURL] 
absoluteString];
+    
     // On iOS 13 the movie path becomes inaccessible, create and return a copy
-    if (IsAtLeastiOSVersion(@"13.0")) {
-        moviePath = [self createTmpVideo:[[info 
objectForKey:UIImagePickerControllerMediaURL] path]];
+    if (@available(iOS 13, *)) {
+        moviePath = [self copyFileToTemp:[[info 
objectForKey:UIImagePickerControllerMediaURL] path]];
     }
+    
     return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK 
messageAsString:moviePath];
 }
 
-- (NSString*)createTmpVideo:(NSString*)moviePath
+/**
+ Generates a unique temporary file path for a file extension.
+ 
+ The filename is prefixed with `cdv_photo_` and suffixed with the provided
+ file extension. A UNIX timestamp in milliseconds since 1970 is used to ensure
+ uniqueness between calls.
+ 
+ Threading: Safe to call from any thread. Uses NSTemporaryDirectory() and
+ does not perform any I/O; it only constructs a path string.
+ 
+ @param fileExtension  The desired file extension without a leading dot
+                      (for example, "jpg", "png", or the original video
+                      extension like "mov").
+ 
+ @return An absolute path string within the app's temporary directory,
+         e.g. 
`/var/mobile/Containers/Data/Application/<UUID>/tmp/cdv_photo_<timestamp>.jpg`.
+ 
+ @discussion The returned path is not created on disk. Callers are responsible
+             for writing data to the path and handling any errors.
+ 
+ @note Only files whose names start with `cdv_photo_` are cleaned up by the
+       plugin's `cleanup:` method.
+ **/
+- (NSString*)tempFilePathForExtension:(NSString*)fileExtension
+{
+    // Return a unique file name like
+    // 
`/var/mobile/Containers/Data/Application/<UUID>/tmp/cdv_photo_<timestamp>.jpg`.
+    return [NSString stringWithFormat:
+            @"%@/%@%lld.%@",
+            [NSTemporaryDirectory() stringByStandardizingPath],
+            CDV_PHOTO_PREFIX,
+            (long long)([[NSDate date] timeIntervalSince1970] * 1000.0),
+            fileExtension];
+}
+
+- (NSString*)copyFileToTemp:(NSString*)filePath
 {
-    NSString* moviePathExtension = [moviePath pathExtension];
-    NSString* copyMoviePath = [self tempFilePath:moviePathExtension];
-    NSFileManager* fileMgr = [[NSFileManager alloc] init];
-    NSError *error;
-    [fileMgr copyItemAtPath:moviePath toPath:copyMoviePath error:&error];
-    return [[NSURL fileURLWithPath:copyMoviePath] absoluteString];
+    NSFileManager* fileManager = [[NSFileManager alloc] init];
+    NSString* tempFilePath = [self tempFilePathForExtension:[filePath 
pathExtension]];
+    NSError *error = nil;
+    
+    // Copy file to temp directory
+    BOOL copySuccess = [fileManager copyItemAtPath:filePath 
toPath:tempFilePath error:&error];
+    
+    if (!copySuccess || error) {
+        NSLog(@"CDVCamera: Failed to copy file from %@ to temporary path %@. 
Error: %@", filePath, tempFilePath, [error localizedDescription]);
+        return nil;
+    }
+    
+    // Verify the copied file exists
+    if (![fileManager fileExistsAtPath:tempFilePath]) {
+        NSLog(@"CDVCamera: Copied file does not exist at temporary path: %@", 
tempFilePath);
+        return nil;
+    }
+    
+    return [[NSURL fileURLWithPath:tempFilePath] absoluteString];
+}
+
+/**
+  Called by JS camera.cleanup()
+  Removes intermediate image files that are kept in temporary storage after
+  calling camera.getPicture.
+*/
+- (void)cleanup:(CDVInvokedUrlCommand*)command
+{
+    NSFileManager* fileManager = [NSFileManager defaultManager];
+    NSString* tempDirectoryPath = NSTemporaryDirectory();
+    NSError* error = nil;
+    
+    NSArray<NSString*>* allFiles = [fileManager 
contentsOfDirectoryAtPath:tempDirectoryPath error:&error];
+    
+    if (error) {
+        CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_IO_EXCEPTION 
+                                                    messageAsString:[error 
localizedDescription]];
+        [self.commandDelegate sendPluginResult:result 
callbackId:command.callbackId];
+        return;
+    }
+    
+    BOOL hasErrors = NO;
+    
+    for (NSString* fileName in allFiles) {
+        // Only delete files created by the camera plugin
+        if (![fileName hasPrefix:CDV_PHOTO_PREFIX]) continue;
+        
+        NSString* filePath = [tempDirectoryPath 
stringByAppendingPathComponent:fileName];
+        NSError* deleteError = nil;
+        
+        if (![fileManager removeItemAtPath:filePath error:&deleteError]) {
+            NSLog(@"Failed to delete: %@ (error: %@)", filePath, deleteError);
+            hasErrors = YES;
+        }
+    }
+    
+    CDVPluginResult* result = hasErrors 
+        ? [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION 
messageAsString:@"One or more files failed to be deleted."]
+        : [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
+    
+    [self.commandDelegate sendPluginResult:result 
callbackId:command.callbackId];
 }
 
 #pragma mark UIImagePickerControllerDelegate methods
@@ -893,6 +946,8 @@ static NSString* MIME_JPEG    = @"image/jpeg";
         __block CDVPluginResult* result = nil;
 
         NSString* mediaType = [info 
objectForKey:UIImagePickerControllerMediaType];
+        
+        // Image selected
         if ([mediaType isEqualToString:(NSString*)kUTTypeImage]) {
             [weakSelf resultForImage:cameraPicker.pictureOptions info:info 
completion:^(CDVPluginResult* res) {
                 if (![self usesGeolocation] || picker.sourceType != 
UIImagePickerControllerSourceTypeCamera) {
@@ -901,8 +956,9 @@ static NSString* MIME_JPEG    = @"image/jpeg";
                     weakSelf.pickerController = nil;
                 }
             }];
-        }
-        else {
+            
+            // Video selected
+        } else {
             result = [weakSelf resultForVideo:info];
             [weakSelf.commandDelegate sendPluginResult:result 
callbackId:cameraPicker.callbackId];
             weakSelf.hasPendingOperation = NO;
@@ -948,6 +1004,11 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 
 #pragma mark CLLocationManager
 
+/**
+    Lazy instantiation of the CLLocationManager used to get GPS location data 
when
+    when capturing JPEGs.
+    @return The CLLocationManager instance.
+*/
 - (CLLocationManager*)locationManager
 {
     if (locationManager != nil) {
@@ -963,6 +1024,14 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 
 # pragma mark CLLocationManagerDelegate methods
 
+/**
+    Called when the CLLocationManager has retrieved a location update.
+    The location data is formatted and added to the image metadata, and
+    the image result is returned. Only used when capturing JPEGs.
+    @param manager The CLLocationManager instance.
+    @param newLocation The new CLLocation data.
+    @param oldLocation The previous CLLocation data.
+*/
 - (void)locationManager:(CLLocationManager*)manager
     didUpdateToLocation:(CLLocation*)newLocation
            fromLocation:(CLLocation*)oldLocation
@@ -1022,6 +1091,13 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     [self imagePickerControllerReturnImageResult];
 }
 
+/**
+    Called when the CLLocationManager fails to retrieve location data.
+    The image result is returned without location metadata.
+    Only used when capturing JPEGs.
+    @param manager The CLLocationManager instance.
+    @param error The error that occurred.
+*/
 - (void)locationManager:(CLLocationManager*)manager 
didFailWithError:(NSError*)error
 {
     if (locationManager == nil) {
@@ -1034,6 +1110,10 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     [self imagePickerControllerReturnImageResult];
 }
 
+/**
+    Called to return the image result after location data has been added to 
the metadata
+    or an error occurred while retrieving location data.
+*/
 - (void)imagePickerControllerReturnImageResult
 {
     CDVPictureOptions* options = self.pickerController.pictureOptions;
@@ -1069,14 +1149,16 @@ static NSString* MIME_JPEG    = @"image/jpeg";
         {
             NSError* err = nil;
             NSString* extension = 
self.pickerController.pictureOptions.encodingType == EncodingTypePNG ? 
@"png":@"jpg";
-            NSString* filePath = [self tempFilePath:extension];
+            NSString* filePath = [self tempFilePathForExtension:extension];
 
             // save file
             if (![self.data writeToFile:filePath options:NSAtomicWrite 
error:&err]) {
-                result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[err 
localizedDescription]];
+                result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_IO_EXCEPTION
+                                           messageAsString:[err 
localizedDescription]];
             }
             else {
-                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK 
messageAsString:[[self urlTransformer:[NSURL fileURLWithPath:filePath]] 
absoluteString]];
+                result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
+                                           messageAsString:[[NSURL 
fileURLWithPath:filePath] absoluteString]];
             }
         }
             break;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to