diff --git a/camera/README.md b/camera/README.md index f7c35a389..5c26e6876 100644 --- a/camera/README.md +++ b/camera/README.md @@ -143,14 +143,15 @@ Request camera and photo album permissions #### Photo -| Prop | Type | Description | Since | -| ------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`base64String`** | string | The base64 encoded string representation of the image, if using CameraResultType.Base64. | 1.0.0 | -| **`dataUrl`** | string | The url starting with 'data:image/jpeg;base64,' and the base64 encoded string representation of the image, if using CameraResultType.DataUrl. | 1.0.0 | -| **`path`** | string | If using CameraResultType.Uri, the path will contain a full, platform-specific file URL that can be read later using the Filsystem API. | 1.0.0 | -| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of an image for efficient loading and rendering. | 1.0.0 | -| **`exif`** | any | Exif data, if any, retrieved from the image | 1.0.0 | -| **`format`** | string | The format of the image, ex: jpeg, png, gif. iOS and Android only support jpeg. Web supports jpeg and png. gif is only supported if using file input. | 1.0.0 | +| Prop | Type | Description | Since | +| ------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| **`base64String`** | string | The base64 encoded string representation of the image, if using CameraResultType.Base64. | 1.0.0 | +| **`dataUrl`** | string | The url starting with 'data:image/jpeg;base64,' and the base64 encoded string representation of the image, if using CameraResultType.DataUrl. | 1.0.0 | +| **`path`** | string | If using CameraResultType.Uri, the path will contain a full, platform-specific file URL that can be read later using the Filsystem API. | 1.0.0 | +| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of an image for efficient loading and rendering. | 1.0.0 | +| **`exif`** | any | Exif data, if any, retrieved from the image | 1.0.0 | +| **`format`** | string | The format of the image, ex: jpeg, png, gif. iOS and Android only support jpeg. Web supports jpeg and png. gif is only supported if using file input. | 1.0.0 | +| **`saved`** | boolean | Whether if the image was saved to the gallery or not. On Android and iOS, saving to the gallery can fail if the user didn't grant the required permissions. On Web there is no gallery, so always returns false. | 1.1.0 | #### ImageOptions diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index b47dea065..95688f2f0 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -78,6 +78,8 @@ public class CameraPlugin extends Plugin { private Uri imageFileUri; private Uri imagePickedContentUri; private boolean isEdited = false; + private boolean isFirstRequest = true; + private boolean isSaved = false; private CameraSettings settings = new CameraSettings(); @@ -145,7 +147,8 @@ private boolean checkCameraPermissions(PluginCall call) { boolean hasPhotoPerms = getPermissionState(PHOTOS) == PermissionState.GRANTED; // If we want to save to the gallery, we need two permissions - if (settings.isSaveToGallery() && !(hasCameraPerms && hasPhotoPerms)) { + if (settings.isSaveToGallery() && !(hasCameraPerms && hasPhotoPerms) && isFirstRequest) { + isFirstRequest = false; String[] aliases; if (needCameraPerms) { aliases = new String[] { CAMERA, PHOTOS }; @@ -391,11 +394,21 @@ private void returnResult(PluginCall call, Bitmap bitmap, Uri u) { boolean saveToGallery = call.getBoolean("saveToGallery", CameraSettings.DEFAULT_SAVE_IMAGE_TO_GALLERY); if (saveToGallery && (imageEditedFileSavePath != null || imageFileSavePath != null)) { + isSaved = true; try { String fileToSavePath = imageEditedFileSavePath != null ? imageEditedFileSavePath : imageFileSavePath; File fileToSave = new File(fileToSavePath); - MediaStore.Images.Media.insertImage(getContext().getContentResolver(), fileToSavePath, fileToSave.getName(), ""); + String inserted = MediaStore.Images.Media.insertImage( + getContext().getContentResolver(), + fileToSavePath, + fileToSave.getName(), + "" + ); + if (inserted == null) { + isSaved = false; + } } catch (FileNotFoundException e) { + isSaved = false; Logger.error(getLogTag(), IMAGE_GALLERY_SAVE_ERROR, e); } } @@ -437,6 +450,7 @@ private void returnFileURI(PluginCall call, ExifWrapper exif, Bitmap bitmap, Uri ret.put("exif", exif.toJson()); ret.put("path", newUri.toString()); ret.put("webPath", FileUtils.getPortablePath(getContext(), bridge.getLocalUrl(), newUri)); + ret.put("saved", isSaved); call.resolve(ret); } else { call.reject(UNABLE_TO_PROCESS_IMAGE); diff --git a/camera/ios/Plugin.xcodeproj/project.pbxproj b/camera/ios/Plugin.xcodeproj/project.pbxproj index 6a94e30c6..f8b8ed64f 100644 --- a/camera/ios/Plugin.xcodeproj/project.pbxproj +++ b/camera/ios/Plugin.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */; }; 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */; }; + 2FA5008E26E9143C00127B0B /* ImageSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5008D26E9143C00127B0B /* ImageSaver.swift */; }; 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFF88201F53D600D50D53 /* Plugin.framework */; }; 50ADFF97201F53D600D50D53 /* CameraPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFF96201F53D600D50D53 /* CameraPluginTests.swift */; }; 50ADFF99201F53D600D50D53 /* CameraPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 50ADFF8B201F53D600D50D53 /* CameraPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -30,6 +31,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 2FA5008D26E9143C00127B0B /* ImageSaver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSaver.swift; sourceTree = ""; }; 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50ADFF88201F53D600D50D53 /* Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50ADFF8B201F53D600D50D53 /* CameraPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CameraPlugin.h; sourceTree = ""; }; @@ -94,6 +96,7 @@ 50ADFF8A201F53D600D50D53 /* Plugin */ = { isa = PBXGroup; children = ( + 2FA5008D26E9143C00127B0B /* ImageSaver.swift */, 50ADFF8B201F53D600D50D53 /* CameraPlugin.h */, 50ADFFA72020EE4F00D50D53 /* CameraPlugin.m */, 50E1A94720377CB70090CE1A /* CameraPlugin.swift */, @@ -311,6 +314,7 @@ files = ( 50E1A94820377CB70090CE1A /* CameraPlugin.swift in Sources */, 50ADFFA82020EE4F00D50D53 /* CameraPlugin.m in Sources */, + 2FA5008E26E9143C00127B0B /* ImageSaver.swift in Sources */, 6276AAD7255B3E1400097815 /* CameraTypes.swift in Sources */, 6276AAD3255B3E0E00097815 /* CameraExtensions.swift in Sources */, ); diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index a137413ac..dbaa2cd24 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -186,23 +186,25 @@ extension CameraPlugin: PHPickerViewControllerDelegate { } private extension CameraPlugin { - func returnProcessedImage(_ processedImage: ProcessedImage) { + func returnImage(_ processedImage: ProcessedImage, isSaved: Bool) { guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else { self.call?.reject("Unable to convert image to jpeg") return } if settings.resultType == CameraResultType.base64 { - call?.resolve([ + self.call?.resolve([ "base64String": jpeg.base64EncodedString(), "exif": processedImage.exifData, - "format": "jpeg" + "format": "jpeg", + "saved": isSaved ]) } else if settings.resultType == CameraResultType.dataURL { call?.resolve([ "dataUrl": "data:image/jpeg;base64," + jpeg.base64EncodedString(), "exif": processedImage.exifData, - "format": "jpeg" + "format": "jpeg", + "saved": isSaved ]) } else if settings.resultType == CameraResultType.uri { guard let fileURL = try? saveTemporaryImage(jpeg), @@ -214,11 +216,27 @@ private extension CameraPlugin { "path": fileURL.absoluteString, "exif": processedImage.exifData, "webPath": webURL.absoluteString, - "format": "jpeg" + "format": "jpeg", + "saved": isSaved ]) } } + func returnProcessedImage(_ processedImage: ProcessedImage) { + // conditionally save the image + if settings.saveToGallery && (processedImage.flags.contains(.edited) == true || processedImage.flags.contains(.gallery) == false) { + _ = ImageSaver(image: processedImage.image) { error in + var isSaved = false + if error == nil { + isSaved = true + } + self.returnImage(processedImage, isSaved: isSaved) + } + } else { + self.returnImage(processedImage, isSaved: false) + } + } + func showPrompt() { // Build the action sheet let alert = UIAlertController(title: settings.userPromptText.title, message: nil, preferredStyle: UIAlertController.Style.actionSheet) @@ -380,11 +398,8 @@ private extension CameraPlugin { metadata = asset.imageData } // get the result - let result = processedImage(from: image, with: metadata) - // conditionally save the image - if settings.saveToGallery && (flags.contains(.edited) == true || flags.contains(.gallery) == false) { - UIImageWriteToSavedPhotosAlbum(result.image, nil, nil, nil) - } + var result = processedImage(from: image, with: metadata) + result.flags = flags return result } diff --git a/camera/ios/Plugin/CameraTypes.swift b/camera/ios/Plugin/CameraTypes.swift index f1bbe130c..382d216e5 100644 --- a/camera/ios/Plugin/CameraTypes.swift +++ b/camera/ios/Plugin/CameraTypes.swift @@ -94,6 +94,7 @@ internal struct PhotoFlags: OptionSet { internal struct ProcessedImage { var image: UIImage var metadata: [String: Any] + var flags: PhotoFlags = [] var exifData: [String: Any] { var exifData = metadata["{Exif}"] as? [String: Any] diff --git a/camera/ios/Plugin/ImageSaver.swift b/camera/ios/Plugin/ImageSaver.swift new file mode 100644 index 000000000..d4c4d74b0 --- /dev/null +++ b/camera/ios/Plugin/ImageSaver.swift @@ -0,0 +1,20 @@ +import UIKit + +class ImageSaver: NSObject { + + var onResult: ((Error?)->Void) = {_ in } + + init(image: UIImage, onResult:@escaping ((Error?)->Void)) { + self.onResult = onResult + super.init() + UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveResult), nil) + } + + @objc func saveResult(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) { + if let error = error { + onResult(error) + } else { + onResult(nil) + } + } +} diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index ca1cc4dc8..7571f2feb 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -208,6 +208,16 @@ export interface Photo { * @since 1.0.0 */ format: string; + /** + * Whether if the image was saved to the gallery or not. + * + * On Android and iOS, saving to the gallery can fail if the user didn't + * grant the required permissions. + * On Web there is no gallery, so always returns false. + * + * @since 1.1.0 + */ + saved: boolean; } export enum CameraSource { diff --git a/camera/src/web.ts b/camera/src/web.ts index 5e59c3114..c7d587aa3 100644 --- a/camera/src/web.ts +++ b/camera/src/web.ts @@ -161,6 +161,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { resolve({ webPath: URL.createObjectURL(photo), format: format, + saved: false, }); } else { reader.readAsDataURL(photo); @@ -170,11 +171,13 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { resolve({ dataUrl: r, format: format, + saved: false, }); } else { resolve({ base64String: r.split(',')[1], format: format, + saved: false, }); } };