From 4fe138fe2a2ef7fda2847e4eff3c368071e5ad75 Mon Sep 17 00:00:00 2001 From: Artal Druk Date: Wed, 22 Mar 2017 18:40:35 +0200 Subject: [PATCH] - support capture/retake mode on the full screen camera on Android - delete the temp file (created in capture/retake) when canceled --- .../com/wix/RNCameraKit/SaveImageTask.java | 235 ++++++++++++++++++ .../wix/RNCameraKit/camera/CameraModule.java | 3 +- .../RNCameraKit/camera/commands/Capture.java | 162 +----------- .../gallery/NativeGalleryModule.java | 26 ++ .../ReactNativeCameraKit/CKGalleryManager.m | 20 ++ src/CameraScreen/CameraKitCameraScreenBase.js | 22 +- 6 files changed, 298 insertions(+), 170 deletions(-) create mode 100644 android/src/main/java/com/wix/RNCameraKit/SaveImageTask.java diff --git a/android/src/main/java/com/wix/RNCameraKit/SaveImageTask.java b/android/src/main/java/com/wix/RNCameraKit/SaveImageTask.java new file mode 100644 index 0000000..890e78a --- /dev/null +++ b/android/src/main/java/com/wix/RNCameraKit/SaveImageTask.java @@ -0,0 +1,235 @@ +package com.wix.RNCameraKit; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.hardware.Camera; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.MediaStore; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Metadata; +import com.drew.metadata.MetadataException; +import com.drew.metadata.exif.ExifIFD0Directory; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.WritableMap; +import com.wix.RNCameraKit.camera.CameraViewManager; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import static com.facebook.react.common.ReactConstants.TAG; + +public class SaveImageTask extends AsyncTask { + + private final Context context; + private final Promise promise; + private boolean saveToCameraRoll; + private String bitmapUrl = null; + + public SaveImageTask(Context context, Promise promise, boolean saveToCameraRoll) { + this.context = context; + this.promise = promise; + this.saveToCameraRoll = saveToCameraRoll; + } + + public SaveImageTask(String bitmapUrl, Context context, Promise promise, boolean saveToCameraRoll) { + this(context, promise, saveToCameraRoll); + this.bitmapUrl = bitmapUrl; + if (this.bitmapUrl != null) { + this.bitmapUrl = this.bitmapUrl.replace("file://",""); + } + } + + private Bitmap getImageBitmap(byte[]... data) { + Bitmap image; + if (bitmapUrl != null) { + FileInputStream fis; + File imageFile = new File(bitmapUrl); + try { + fis = new FileInputStream(imageFile); + image = BitmapFactory.decodeStream(fis); + fis.close(); + } catch (IOException e) { + e.printStackTrace(); + image = null; + } + + if (imageFile.exists()) { + imageFile.delete(); + } + } + else { + byte[] rawImageData = data[0]; + image = decodeAndRotateIfNeeded(rawImageData); + } + return image; + } + + @Override + protected Void doInBackground(byte[]... data) { + Bitmap image = getImageBitmap(data); + if (image == null) { + promise.reject("CameraKit", "failed to get Bitmap image"); + return null; + } + + WritableMap imageInfo = saveToCameraRoll ? saveToMediaStore(image) : saveTempImageFile(image); + if (imageInfo == null) + promise.reject("CameraKit", "failed to save image to MediaStore"); + else { + promise.resolve(imageInfo); + CameraViewManager.reconnect(); + } + return null; + } + + private WritableMap createImageInfo(String filePath, String id, String fileName, long fileSize) { + WritableMap imageInfo = Arguments.createMap(); + imageInfo.putString("uri", filePath); + imageInfo.putString("id", id); + imageInfo.putString("name", fileName); + imageInfo.putInt("size", (int) fileSize); + return imageInfo; + } + + private WritableMap saveToMediaStore(Bitmap image) { + try { + String fileUri = MediaStore.Images.Media.insertImage(context.getContentResolver(), image, System.currentTimeMillis() + "", ""); + Cursor cursor = context.getContentResolver().query(Uri.parse(fileUri), new String[]{ + MediaStore.Images.ImageColumns.DATA, + MediaStore.Images.ImageColumns.DISPLAY_NAME + }, null, null, null); + cursor.moveToFirst(); + int pathIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA); + int nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DISPLAY_NAME); + String filePath = cursor.getString(pathIndex); + String fileName = cursor.getString(nameIndex); + long fileSize = new File(filePath).length(); + cursor.close(); + + return createImageInfo(filePath, filePath, fileName, fileSize); + } catch (Exception e) { + return null; + } + } + + private Bitmap decodeAndRotateIfNeeded(byte[] rawImageData) { + Matrix bitmapMatrix = getRotationMatrix(rawImageData); + Bitmap image = BitmapFactory.decodeByteArray(rawImageData, 0, rawImageData.length); + if (bitmapMatrix.isIdentity()) + return image; + else + return rotateImage(image, bitmapMatrix); + } + + private Bitmap rotateImage(Bitmap image, Matrix bitmapMatrix) { + return Bitmap.createBitmap(image, 0, 0, image.getWidth(), image.getHeight(), bitmapMatrix, false); + } + + private Matrix getRotationMatrix(byte[] rawImageData) { + try { + return tryGetRotationMatrix(rawImageData); + } catch (Exception e) { + return new Matrix(); + } + } + + private Matrix tryGetRotationMatrix(byte[] rawImageData) throws ImageProcessingException, IOException, MetadataException { + Matrix matrix = new Matrix(); + Metadata metadata = readMetadata(rawImageData); + final ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + boolean hasOrientation = exifIFD0Directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION); + if (hasOrientation) { + final int exifOrientation = exifIFD0Directory.getInt(ExifIFD0Directory.TAG_ORIENTATION); + boolean isFacingFront = CameraViewManager.getCameraInfo().facing == Camera.CameraInfo.CAMERA_FACING_FRONT; + convertExifOrientationToMatrix(matrix, exifOrientation, isFacingFront); + } + return matrix; + } + + private void convertExifOrientationToMatrix(Matrix matrix, int exifOrientation, boolean isCameraFacingFront) { + switch (exifOrientation) { + case 1: + break; // top left + case 2: + matrix.postScale(-1, 1); + break; // top right + case 3: + matrix.postRotate(180); + break; // bottom right + case 4: + matrix.postRotate(180); + matrix.postScale(-1, 1); + break; // bottom left + case 5: + matrix.postRotate(90); + matrix.postScale(-1, 1); + break; // left top + case 6: + matrix.postRotate(90); + break; // right top + case 7: + matrix.postRotate(270); + matrix.postScale(-1, 1); + break; // right bottom + case 8: + matrix.postRotate(270); + break; // left bottom + default: + break; // Unknown + } + if (isCameraFacingFront) { + matrix.postRotate(180); + } + } + + private Metadata readMetadata(byte[] rawImageData) throws ImageProcessingException, IOException { + Metadata metadata = null; + ByteArrayInputStream inputStream = null; + BufferedInputStream bufferedInputStream = null; + try { + inputStream = new ByteArrayInputStream(rawImageData); + bufferedInputStream = new BufferedInputStream(inputStream); + metadata = ImageMetadataReader.readMetadata(bufferedInputStream, rawImageData.length); + } finally { + if (bufferedInputStream != null) bufferedInputStream.close(); + if (inputStream != null) inputStream.close(); + } + return metadata; + } + + @Nullable + private WritableMap saveTempImageFile(Bitmap image) { + File imageFile; + FileOutputStream outputStream; + + Long tsLong = System.currentTimeMillis()/1000; + String fileName = "temp_Image_" + tsLong.toString() + ".jpg"; + + try { + imageFile = new File(context.getCacheDir(), fileName); + if (imageFile.exists()) { + imageFile.delete(); + } + outputStream = new FileOutputStream(imageFile); + image.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); + outputStream.close(); + } catch (IOException e) { + Log.d(TAG, "Error accessing file: " + e.getMessage()); + imageFile = null; + } + return (imageFile != null) ? createImageInfo(Uri.fromFile(imageFile).toString(), imageFile.getAbsolutePath(), fileName, imageFile.length()) : null; + } +} diff --git a/android/src/main/java/com/wix/RNCameraKit/camera/CameraModule.java b/android/src/main/java/com/wix/RNCameraKit/camera/CameraModule.java index 0aab3bb..3cf1c2d 100644 --- a/android/src/main/java/com/wix/RNCameraKit/camera/CameraModule.java +++ b/android/src/main/java/com/wix/RNCameraKit/camera/CameraModule.java @@ -10,6 +10,7 @@ import com.facebook.react.bridge.ReactMethod; import com.wix.RNCameraKit.camera.commands.Capture; import com.wix.RNCameraKit.camera.permission.CameraPermission; + public class CameraModule extends ReactContextBaseJavaModule { private final CameraPermission cameraPermission; @@ -106,7 +107,7 @@ public class CameraModule extends ReactContextBaseJavaModule { @ReactMethod public void capture(boolean saveToCameraRoll, final Promise promise) { - new Capture(getReactApplicationContext()).execute(promise); + new Capture(getReactApplicationContext(), saveToCameraRoll).execute(promise); } public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { diff --git a/android/src/main/java/com/wix/RNCameraKit/camera/commands/Capture.java b/android/src/main/java/com/wix/RNCameraKit/camera/commands/Capture.java index eb1cb45..a21f178 100644 --- a/android/src/main/java/com/wix/RNCameraKit/camera/commands/Capture.java +++ b/android/src/main/java/com/wix/RNCameraKit/camera/commands/Capture.java @@ -1,36 +1,21 @@ package com.wix.RNCameraKit.camera.commands; import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Matrix; import android.hardware.Camera; -import android.net.Uri; -import android.os.AsyncTask; -import android.provider.MediaStore; -import com.drew.imaging.ImageMetadataReader; -import com.drew.imaging.ImageProcessingException; -import com.drew.metadata.Metadata; -import com.drew.metadata.MetadataException; -import com.drew.metadata.exif.ExifIFD0Directory; -import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; -import com.facebook.react.bridge.WritableMap; -import com.wix.RNCameraKit.camera.CameraViewManager; -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; +import com.wix.RNCameraKit.camera.CameraViewManager; +import com.wix.RNCameraKit.SaveImageTask; public class Capture implements Command { private final Context context; + private boolean saveToCameraRoll; - public Capture(Context context) { + public Capture(Context context, boolean saveToCameraRoll) { this.context = context; + this.saveToCameraRoll = saveToCameraRoll; } @Override @@ -47,143 +32,8 @@ public class Capture implements Command { @Override public void onPictureTaken(byte[] data, Camera camera) { camera.stopPreview(); - new SaveImageTask(promise).execute(data); + new SaveImageTask(context, promise, saveToCameraRoll).execute(data); } }); } - - private class SaveImageTask extends AsyncTask { - - private final Promise promise; - - private SaveImageTask(Promise promise) { - this.promise = promise; - } - - @Override - protected Void doInBackground(byte[]... data) { - byte[] rawImageData = data[0]; - Bitmap image = decodeAndRotateIfNeeded(rawImageData); - WritableMap imageInfo = saveToMediaStore(image); - if (imageInfo == null) - promise.reject("CameraKit", "failed to save image to MediaStore"); - else { - promise.resolve(imageInfo); - CameraViewManager.reconnect(); - } - return null; - } - - private WritableMap saveToMediaStore(Bitmap image) { - try { - String fileUri = MediaStore.Images.Media.insertImage(context.getContentResolver(), image, System.currentTimeMillis() + "", ""); - Cursor cursor = context.getContentResolver().query(Uri.parse(fileUri), new String[]{ - MediaStore.Images.ImageColumns.DATA, - MediaStore.Images.ImageColumns.DISPLAY_NAME - }, null, null, null); - cursor.moveToFirst(); - int pathIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA); - int nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DISPLAY_NAME); - String filePath = cursor.getString(pathIndex); - String fileName = cursor.getString(nameIndex); - long fileSize = new File(filePath).length(); - cursor.close(); - - WritableMap imageInfo = Arguments.createMap(); - imageInfo.putString("uri", filePath); - imageInfo.putString("id", filePath); - imageInfo.putString("name", fileName); - imageInfo.putInt("size", (int) fileSize); - - return imageInfo; - } catch (Exception e) { - return null; - } - } - - private Bitmap decodeAndRotateIfNeeded(byte[] rawImageData) { - Matrix bitmapMatrix = getRotationMatrix(rawImageData); - Bitmap image = BitmapFactory.decodeByteArray(rawImageData, 0, rawImageData.length); - if (bitmapMatrix.isIdentity()) - return image; - else - return rotateImage(image, bitmapMatrix); - } - - private Bitmap rotateImage(Bitmap image, Matrix bitmapMatrix) { - return Bitmap.createBitmap(image, 0, 0, image.getWidth(), image.getHeight(), bitmapMatrix, false); - } - - private Matrix getRotationMatrix(byte[] rawImageData) { - try { - return tryGetRotationMatrix(rawImageData); - } catch (Exception e) { - return new Matrix(); - } - } - - private Matrix tryGetRotationMatrix(byte[] rawImageData) throws ImageProcessingException, IOException, MetadataException { - Matrix matrix = new Matrix(); - Metadata metadata = readMetadata(rawImageData); - final ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); - boolean hasOrientation = exifIFD0Directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION); - if (hasOrientation) { - final int exifOrientation = exifIFD0Directory.getInt(ExifIFD0Directory.TAG_ORIENTATION); - boolean isFacingFront = CameraViewManager.getCameraInfo().facing == Camera.CameraInfo.CAMERA_FACING_FRONT; - convertExifOrientationToMatrix(matrix, exifOrientation, isFacingFront); - } - return matrix; - } - - private void convertExifOrientationToMatrix(Matrix matrix, int exifOrientation, boolean isCameraFacingFront) { - switch (exifOrientation) { - case 1: - break; // top left - case 2: - matrix.postScale(-1, 1); - break; // top right - case 3: - matrix.postRotate(180); - break; // bottom right - case 4: - matrix.postRotate(180); - matrix.postScale(-1, 1); - break; // bottom left - case 5: - matrix.postRotate(90); - matrix.postScale(-1, 1); - break; // left top - case 6: - matrix.postRotate(90); - break; // right top - case 7: - matrix.postRotate(270); - matrix.postScale(-1, 1); - break; // right bottom - case 8: - matrix.postRotate(270); - break; // left bottom - default: - break; // Unknown - } - if (isCameraFacingFront) { - matrix.postRotate(180); - } - } - - private Metadata readMetadata(byte[] rawImageData) throws ImageProcessingException, IOException { - Metadata metadata = null; - ByteArrayInputStream inputStream = null; - BufferedInputStream bufferedInputStream = null; - try { - inputStream = new ByteArrayInputStream(rawImageData); - bufferedInputStream = new BufferedInputStream(inputStream); - metadata = ImageMetadataReader.readMetadata(bufferedInputStream, rawImageData.length); - } finally { - if (bufferedInputStream != null) bufferedInputStream.close(); - if (inputStream != null) inputStream.close(); - } - return metadata; - } - } } diff --git a/android/src/main/java/com/wix/RNCameraKit/gallery/NativeGalleryModule.java b/android/src/main/java/com/wix/RNCameraKit/gallery/NativeGalleryModule.java index 86226bf..5393df6 100644 --- a/android/src/main/java/com/wix/RNCameraKit/gallery/NativeGalleryModule.java +++ b/android/src/main/java/com/wix/RNCameraKit/gallery/NativeGalleryModule.java @@ -2,6 +2,7 @@ package com.wix.RNCameraKit.gallery; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.provider.MediaStore; import android.support.annotation.NonNull; @@ -14,8 +15,12 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.wix.RNCameraKit.SaveImageTask; import com.wix.RNCameraKit.gallery.permission.StoragePermission; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.util.Collection; import java.util.HashMap; @@ -235,4 +240,25 @@ public class NativeGalleryModule extends ReactContextBaseJavaModule { ret.putArray("images", arr); promise.resolve(ret); } + + @ReactMethod + public void saveImageURLToCameraRoll(String imageUrl, final Promise promise) { + new SaveImageTask(imageUrl, getReactApplicationContext(), promise, true).execute(); + } + + @ReactMethod + public void deleteTempImage(String imageUrl, final Promise promise) { + boolean success = true; + String imagePath = imageUrl.replace("file://",""); + File imageFile = new File(imagePath); + if (imageFile.exists()) { + success = imageFile.delete(); + } + + if(promise != null) { + WritableMap result = Arguments.createMap(); + result.putBoolean("success", success); + promise.resolve(result); + } + } } diff --git a/ios/lib/ReactNativeCameraKit/CKGalleryManager.m b/ios/lib/ReactNativeCameraKit/CKGalleryManager.m index 27b6277..54e394e 100644 --- a/ios/lib/ReactNativeCameraKit/CKGalleryManager.m +++ b/ios/lib/ReactNativeCameraKit/CKGalleryManager.m @@ -350,4 +350,24 @@ RCT_EXPORT_METHOD(saveImageURLToCameraRoll:(NSString*)imageURL }]; } +RCT_EXPORT_METHOD(deleteTempImage:(NSString*)tempImageURL + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSError *error; + NSFileManager *defaultFileManager = [NSFileManager defaultManager]; + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:@{@"success": @(YES)}]; + tempImageURL = [tempImageURL stringByReplacingOccurrencesOfString:@"file://" withString:@""]; + if([defaultFileManager fileExistsAtPath:tempImageURL]) { + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:tempImageURL error:&error]; + result[@"success"] = @(success); + if(error) { + result[@"error"] = [error description]; + } + } + + if(resolve) { + resolve(result); + } +} + @end diff --git a/src/CameraScreen/CameraKitCameraScreenBase.js b/src/CameraScreen/CameraKitCameraScreenBase.js index b1c981b..c437c0a 100644 --- a/src/CameraScreen/CameraKitCameraScreenBase.js +++ b/src/CameraScreen/CameraKitCameraScreenBase.js @@ -12,7 +12,7 @@ import _ from 'lodash'; import CameraKitCamera from './../CameraKitCamera'; const IsIOS = Platform.OS === 'ios'; -const CKGallery = IsIOS ? NativeModules.CKGalleryManager : null; +const GalleryManager = IsIOS ? NativeModules.CKGalleryManager : NativeModules.NativeGalleryModule; const FLASH_MODE_AUTO = 'auto'; const FLASH_MODE_ON = 'on'; @@ -129,12 +129,12 @@ export default class CameraScreenBase extends Component { { this.isCaptureRetakeMode() ? : this.camera = cam} - style={{ flex: 1, justifyContent: 'flex-end' }} + style={{flex: 1, justifyContent: 'flex-end'}} cameraOptions={this.state.cameraOptions} /> } @@ -201,19 +201,15 @@ export default class CameraScreenBase extends Component { const captureRetakeMode = this.isCaptureRetakeMode(); if (captureRetakeMode) { if(type === 'left') { + GalleryManager.deleteTempImage(this.state.imageCaptured.uri); this.setState({imageCaptured: undefined}); } - if(type === 'right') { - if(CKGallery !== null) { - const result = await CKGallery.saveImageURLToCameraRoll(this.state.imageCaptured.uri); - const savedImage = {...this.state.imageCaptured, id: result.id}; - this.setState({imageCaptured: undefined, captureImages: _.concat(this.state.captureImages, savedImage)}, () => { - this.sendBottomButtonPressedAction(type, captureRetakeMode); - }); - } else { - console.warn('Not implemented on Android yet'); + else if(type === 'right') { + const result = await GalleryManager.saveImageURLToCameraRoll(this.state.imageCaptured.uri); + const savedImage = {...this.state.imageCaptured, id: result.id}; + this.setState({imageCaptured: undefined, captureImages: _.concat(this.state.captureImages, savedImage)}, () => { this.sendBottomButtonPressedAction(type, captureRetakeMode); - } + }); } } else { this.sendBottomButtonPressedAction(type, captureRetakeMode);