diff --git a/.npmignore b/.npmignore index 8bd0bef46..436c36723 100644 --- a/.npmignore +++ b/.npmignore @@ -2,3 +2,4 @@ example node_modules build +images diff --git a/README.md b/README.md index 1ce96d4c6..2c42512d3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # react-native-image-crop-picker -iOS/Android image picker with support for camera, configurable compression, multiple images and cropping [![Backers on Open Collective](https://opencollective.com/react-native-image-crop-picker/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/react-native-image-crop-picker/sponsors/badge.svg)](#sponsors) + + +iOS/Android image picker with support for camera, video, configurable compression, multiple images and cropping + ## Result

@@ -11,6 +14,10 @@ iOS/Android image picker with support for camera, configurable compression, mult

+## Important note + +If you are using react-native >= 0.60 use react-native-image-crop-picker version >= 0.25.0. Otherwise use version < 0.25.0. + ## Usage Import library @@ -55,13 +62,25 @@ ImagePicker.openPicker({ **Android: The prop 'cropping' has been known to cause videos not to be display in the gallery on Android. Please do not set cropping to true when selecting videos.** -### Select from camera +### Select from camera + +#### Image ```javascript ImagePicker.openCamera({ width: 300, height: 400, - cropping: true + cropping: true, +}).then(image => { + console.log(image); +}); +``` + +#### Video + +```javascript +ImagePicker.openCamera({ + mediaType: 'video', }).then(image => { console.log(image); }); @@ -100,8 +119,9 @@ ImagePicker.clean().then(() => { | height | number | Height of result image when used with `cropping` option | | multiple | bool (default false) | Enable or disable multiple image selection | | writeTempFile (ios only) | bool (default true) | When set to false, does not write temporary files for the selected images. This is useful to improve performance when you are retrieving file contents with the `includeBase64` option and don't need to read files from disk. | -| includeBase64 | bool (default false) | Enable or disable returning base64 data with image | +| includeBase64 | bool (default false) | When set to true, the image file content will be available as a base64-encoded string in the `data` property. Hint: To use this string as an image source, use it like: ```` | | includeExif | bool (default false) | Include image exif data in the response | +| avoidEmptySpaceAroundImage | bool (default true) | When set to true, the image will always fill the mask space. | | cropperActiveWidgetColor (android only) | string (default `"#424242"`) | When cropping image, determines ActiveWidget color. | | cropperStatusBarColor (android only) | string (default `#424242`) | When cropping image, determines the color of StatusBar. | | cropperToolbarColor (android only) | string (default `#424242`) | When cropping image, determines the color of Toolbar. | @@ -113,17 +133,21 @@ ImagePicker.clean().then(() => { | maxFiles (ios only) | number (default 5) | Max number of files to select when using `multiple` option | | waitAnimationEnd (ios only) | bool (default true) | Promise will resolve/reject once ViewController `completion` block is called | | smartAlbums (ios only) | array ([supported values](https://github.com/ivpusic/react-native-image-crop-picker/blob/master/README.md#smart-album-types-ios)) (default ['UserLibrary', 'PhotoStream', 'Panoramas', 'Videos', 'Bursts']) | List of smart albums to choose from | -| useFrontCamera (ios only) | bool (default false) | Whether to default to the front/'selfie' camera when opened | +| useFrontCamera | bool (default false) | Whether to default to the front/'selfie' camera when opened. Please note that not all Android devices handle this parameter, see [issue #1058](https://github.com/ivpusic/react-native-image-crop-picker/issues/1058)| | compressVideoPreset (ios only) | string (default MediumQuality) | Choose which preset will be used for video compression | | compressImageMaxWidth | number (default none) | Compress image with maximum width | | compressImageMaxHeight | number (default none) | Compress image with maximum height | -| compressImageQuality | number (default 1) | Compress image with quality (from 0 to 1, where 1 is best quality) | +| compressImageQuality | number (default 1 (Android)/0.8 (iOS)) | Compress image with quality (from 0 to 1, where 1 is best quality). On iOS, values larger than 0.8 don't produce a noticable quality increase in most images, while a value of 0.8 will reduce the file size by about half or less compared to a value of 1. | | loadingLabelText (ios only) | string (default "Processing assets...") | Text displayed while photo is loading in picker | | mediaType | string (default any) | Accepted mediaType for image selection, can be one of: 'photo', 'video', or 'any' | | showsSelectedCount (ios only) | bool (default true) | Whether to show the number of selected assets | +| forceJpg (ios only) | bool (default false) | Whether to convert photos to JPG. This will also convert any Live Photo into its JPG representation | | showCropGuidelines (android only) | bool (default true) | Whether to show the 3x3 grid on top of the image during cropping | +| showCropFrame (android only) | bool (default true) | Whether to show crop frame during cropping | | hideBottomControls (android only) | bool (default false) | Whether to display bottom controls | | enableRotationGesture (android only) | bool (default false) | Whether to enable rotating the image by hand gesture | +| cropperChooseText (ios only)  |           string (default choose)        | Choose button text | +| cropperCancelText (ios only) | string (default Cancel) | Cancel button text | #### Smart Album Types (ios) @@ -161,6 +185,17 @@ npm i react-native-image-crop-picker --save ### iOS +NOTE: If you are using react-native >= 0.60 autolinking, all you have to do is: + +- Install the library via NPM or Yarm +- Run the following: +``` +cd ios +pod install +``` + +Then the library will be successfully linked. + #### - If you use Cocoapods which is highly recommended: ```bash @@ -219,8 +254,18 @@ After this use `ios/.xcworkspace`. **Do not use** `ios/ '../node_modules/react-native-image-crop-picker/ios/QBImagePicker/QBImagePickerController.podspec' +``` + ### Android +NOTE: If you are using react-native >= 0.60 autolinking, you can skip this step. + ```bash react-native link react-native-image-crop-picker ``` @@ -237,14 +282,21 @@ In Xcode open Info.plist and add string key `NSPhotoLibraryUsageDescription` wit ##### Only if you are not using Cocoapods -- Drag and drop the ios/ImageCropPickerSDK folder to your xcode project. (Make sure Copy items if needed IS ticked) - Click on project General tab - Under `Deployment Info` set `Deployment Target` to `8.0` - Under `Embedded Binaries` click `+` and add `RSKImageCropper.framework` and `QBImagePicker.framework` + +#### Step Optional - To localizate the camera / gallery text buttons + +- Open your Xcode project +- Go to your project settings by opening the project name on the Navigation (left side) +- Select your project in the project list +- Should be into the Info tab and add in Localizations the language your app was missing throughout the + +- Rebuild and you should now have your app camera and gallery with the classic ios text in the language you added. ### Android -- Make sure you are using Gradle `2.2.x` (android/build.gradle) +- Make sure you are using Gradle >= `2.2.x` (android/build.gradle) ```gradle buildscript { @@ -266,8 +318,11 @@ allprojects { jcenter() maven { url "$rootDir/../node_modules/react-native/android" } - // jitpack repo is necessary to fetch ucrop dependency - maven { url "https://jitpack.io" } + // ADD THIS + maven { url 'https://maven.google.com' } + + // ADD THIS + maven { url "https://www.jitpack.io" } } } ``` @@ -287,9 +342,30 @@ android { } ``` +- Use Android SDK >= 26 (android/app/build.gradle) + +```gradle +android { + compileSdkVersion 27 + buildToolsVersion "27.0.3" + ... + + defaultConfig { + ... + targetSdkVersion 27 + ... + } + ... +} +``` + - [Optional] If you want to use camera picker in your project, add following to `app\src\main\AndroidManifest.xml` - `` +- [Optional] If you want to use front camera, also add following to `app\src\main\AndroidManifest.xml` + - `` + - `` + ## Production build ### iOS @@ -312,7 +388,6 @@ Details for second approach: ## TO DO - [ ] [Android] Standardize multiple select -- [ ] [Android] Pick remote media - [ ] [Android] Video compression diff --git a/RNImageCropPicker.podspec b/RNImageCropPicker.podspec index 0667cc4c4..f84acdc36 100644 --- a/RNImageCropPicker.podspec +++ b/RNImageCropPicker.podspec @@ -14,5 +14,6 @@ Pod::Spec.new do |s| s.platform = :ios, "8.0" s.dependency 'RSKImageCropper' s.dependency 'QBImagePickerController' - s.dependency 'React/Core' + s.dependency 'React-Core' + s.dependency 'React-RCTImage' end diff --git a/android/build.gradle b/android/build.gradle index 6a45d5428..229d7cb11 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,17 @@ apply plugin: 'com.android.library' +def DEFAULT_COMPILE_SDK_VERSION = 28 +def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3" +def DEFAULT_TARGET_SDK_VERSION = 28 +def DEFAULT_MIN_SDK_VERSION = 16 + android { - compileSdkVersion 27 - buildToolsVersion "27.0.0" + compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION + buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION - defaultConfig { - minSdkVersion 16 - targetSdkVersion 27 + defaultConfig { + minSdkVersion rootProject.hasProperty('minSdkVersion') ? rootProject.minSdkVersion : DEFAULT_MIN_SDK_VERSION + targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION versionCode 1 } lintOptions { @@ -15,7 +20,6 @@ android { } dependencies { - compile 'com.facebook.react:react-native:+' - compile 'com.github.yalantis:ucrop:2.2.1-native' - compile 'id.zelory:compressor:2.1.0' + implementation 'com.facebook.react:react-native:+' + implementation 'com.github.yalantis:ucrop:2.2.2-native' } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 4c7d29ae1..187de42c8 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ @@ -17,7 +17,6 @@ diff --git a/android/src/main/java/com/reactnative/ivpusic/imagepicker/Compression.java b/android/src/main/java/com/reactnative/ivpusic/imagepicker/Compression.java index 9b06e4634..b6070fdb4 100644 --- a/android/src/main/java/com/reactnative/ivpusic/imagepicker/Compression.java +++ b/android/src/main/java/com/reactnative/ivpusic/imagepicker/Compression.java @@ -2,14 +2,23 @@ import android.app.Activity; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.media.ExifInterface; import android.os.Environment; import android.util.Log; + import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReadableMap; -import id.zelory.compressor.Compressor; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; /** * Created by ipusic on 12/27/16. @@ -17,50 +26,104 @@ class Compression { - File compressImage(final Activity activity, final ReadableMap options, final String originalImagePath) throws IOException { + File resize(String originalImagePath, int maxWidth, int maxHeight, int quality) throws IOException { + Bitmap original = BitmapFactory.decodeFile(originalImagePath); + + int width = original.getWidth(); + int height = original.getHeight(); + + // Use original image exif orientation data to preserve image orientation for the resized bitmap + ExifInterface originalExif = new ExifInterface(originalImagePath); + int originalOrientation = originalExif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1); + + Matrix rotationMatrix = new Matrix(); + int rotationAngleInDegrees = getRotationInDegreesForOrientationTag(originalOrientation); + rotationMatrix.postRotate(rotationAngleInDegrees); + + float ratioBitmap = (float) width / (float) height; + float ratioMax = (float) maxWidth / (float) maxHeight; + + int finalWidth = maxWidth; + int finalHeight = maxHeight; + + if (ratioMax > 1) { + finalWidth = (int) ((float) maxHeight * ratioBitmap); + } else { + finalHeight = (int) ((float) maxWidth / ratioBitmap); + } + + Bitmap resized = Bitmap.createScaledBitmap(original, finalWidth, finalHeight, true); + resized = Bitmap.createBitmap(resized, 0, 0, finalWidth, finalHeight, rotationMatrix, true); + + File imageDirectory = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES); + + if(!imageDirectory.exists()) { + Log.d("image-crop-picker", "Pictures Directory is not existing. Will create this directory."); + imageDirectory.mkdirs(); + } + + File resizeImageFile = new File(imageDirectory, UUID.randomUUID() + ".jpg"); + + OutputStream os = new BufferedOutputStream(new FileOutputStream(resizeImageFile)); + resized.compress(Bitmap.CompressFormat.JPEG, quality, os); + + os.close(); + original.recycle(); + resized.recycle(); + + return resizeImageFile; + } + + int getRotationInDegreesForOrientationTag(int orientationTag) { + switch(orientationTag){ + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_270: + return -90; + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + default: + return 0; + } + } + + File compressImage(final ReadableMap options, final String originalImagePath, final BitmapFactory.Options bitmapOptions) throws IOException { Integer maxWidth = options.hasKey("compressImageMaxWidth") ? options.getInt("compressImageMaxWidth") : null; Integer maxHeight = options.hasKey("compressImageMaxHeight") ? options.getInt("compressImageMaxHeight") : null; Double quality = options.hasKey("compressImageQuality") ? options.getDouble("compressImageQuality") : null; - if (maxWidth == null && maxHeight == null && quality == null) { + boolean isLossLess = (quality == null || quality == 1.0); + boolean useOriginalWidth = (maxWidth == null || maxWidth >= bitmapOptions.outWidth); + boolean useOriginalHeight = (maxHeight == null || maxHeight >= bitmapOptions.outHeight); + + List knownMimes = Arrays.asList("image/jpeg", "image/jpg", "image/png", "image/gif", "image/tiff"); + boolean isKnownMimeType = (bitmapOptions.outMimeType != null && knownMimes.contains(bitmapOptions.outMimeType.toLowerCase())); + + if (isLossLess && useOriginalWidth && useOriginalHeight && isKnownMimeType) { Log.d("image-crop-picker", "Skipping image compression"); return new File(originalImagePath); } Log.d("image-crop-picker", "Image compression activated"); - Compressor compressor = new Compressor(activity) - .setCompressFormat(Bitmap.CompressFormat.JPEG) - .setDestinationDirectoryPath(Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES).getAbsolutePath()); - - if (quality == null) { - Log.d("image-crop-picker", "Compressing image with quality 100"); - compressor.setQuality(100); - } else { - Log.d("image-crop-picker", "Compressing image with quality " + (quality * 100)); - compressor.setQuality((int) (quality * 100)); - } - if (maxWidth != null) { - Log.d("image-crop-picker", "Compressing image with max width " + maxWidth); - compressor.setMaxWidth(maxWidth); - } + // compression quality + int targetQuality = quality != null ? (int) (quality * 100) : 100; + Log.d("image-crop-picker", "Compressing image with quality " + targetQuality); - if (maxHeight != null) { - Log.d("image-crop-picker", "Compressing image with max height " + maxHeight); - compressor.setMaxHeight(maxHeight); + if (maxWidth == null) { + maxWidth = bitmapOptions.outWidth; + } else { + maxWidth = Math.min(maxWidth, bitmapOptions.outWidth); } - File image = new File(originalImagePath); + if (maxHeight == null) { + maxHeight = bitmapOptions.outHeight; + } else { + maxHeight = Math.min(maxHeight, bitmapOptions.outHeight); + } - String[] paths = image.getName().split("\\.(?=[^\\.]+$)"); - String compressedFileName = paths[0] + "-compressed"; - - if(paths.length > 1) - compressedFileName += "." + paths[1]; - - return compressor - .compressToFile(image, compressedFileName); + return resize(originalImagePath, maxWidth, maxHeight, targetQuality); } synchronized void compressVideo(final Activity activity, final ReadableMap options, final String originalVideo, final String compressedVideo, final Promise promise) { diff --git a/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java index b17629b68..c1a2cc5b9 100644 --- a/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java +++ b/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java @@ -14,11 +14,12 @@ import android.os.Build; import android.os.Environment; import android.provider.MediaStore; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.FileProvider; import android.util.Base64; import android.webkit.MimeTypeMap; +import androidx.core.app.ActivityCompat; +import androidx.core.content.FileProvider; + import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.Promise; @@ -65,10 +66,6 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi private static final String E_CANNOT_LAUNCH_CAMERA = "E_CANNOT_LAUNCH_CAMERA"; private static final String E_PERMISSIONS_MISSING = "E_PERMISSION_MISSING"; private static final String E_ERROR_WHILE_CLEANING_FILES = "E_ERROR_WHILE_CLEANING_FILES"; - //Grey 800 - private final String DEFAULT_TINT = "#424242"; - //Light Blue 500 - private final String DEFAULT_WIDGET_COLOR = "#03A9F4"; private String mediaType = "any"; private boolean multiple = false; @@ -78,22 +75,31 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi private boolean cropperCircleOverlay = false; private boolean freeStyleCropEnabled = false; private boolean showCropGuidelines = true; + private boolean showCropFrame = true; private boolean hideBottomControls = false; private boolean enableRotationGesture = false; private boolean disableCropperColorSetters = false; + private boolean useFrontCamera = false; private ReadableMap options; + + //Grey 800 + private final String DEFAULT_TINT = "#424242"; private String cropperActiveWidgetColor = DEFAULT_TINT; private String cropperStatusBarColor = DEFAULT_TINT; private String cropperToolbarColor = DEFAULT_TINT; private String cropperToolbarTitle = null; - private int width = 200; - private int height = 200; + + //Light Blue 500 + private final String DEFAULT_WIDGET_COLOR = "#03A9F4"; + private int width = 0; + private int height = 0; + private Uri mCameraCaptureURI; - private String mCurrentPhotoPath; - private ResultCollector resultCollector; + private String mCurrentMediaPath; + private ResultCollector resultCollector = new ResultCollector(); private Compression compression = new Compression(); - private ReactApplicationContext reactContext = null; - + private ReactApplicationContext reactContext; + PickerModule(ReactApplicationContext reactContext) { super(reactContext); reactContext.addActivityEventListener(this); @@ -114,8 +120,8 @@ private static WritableMap getCroppedRectMap(Intent data) { private String getTmpDir(Activity activity) { String tmpDir = activity.getCacheDir() + "/react-native-image-crop-picker"; - Boolean created = new File(tmpDir).mkdir(); - + new File(tmpDir).mkdir(); + return tmpDir; } @@ -125,23 +131,25 @@ public String getName() { } private void setConfiguration(final ReadableMap options) { - mediaType = options.hasKey("mediaType") ? options.getString("mediaType") : mediaType; + mediaType = options.hasKey("mediaType") ? options.getString("mediaType") : "any"; multiple = options.hasKey("multiple") && options.getBoolean("multiple"); includeBase64 = options.hasKey("includeBase64") && options.getBoolean("includeBase64"); includeExif = options.hasKey("includeExif") && options.getBoolean("includeExif"); - width = options.hasKey("width") ? options.getInt("width") : width; - height = options.hasKey("height") ? options.getInt("height") : height; - cropping = options.hasKey("cropping") ? options.getBoolean("cropping") : cropping; - cropperActiveWidgetColor = options.hasKey("cropperActiveWidgetColor") ? options.getString("cropperActiveWidgetColor") : cropperActiveWidgetColor; - cropperStatusBarColor = options.hasKey("cropperStatusBarColor") ? options.getString("cropperStatusBarColor") : cropperStatusBarColor; - cropperToolbarColor = options.hasKey("cropperToolbarColor") ? options.getString("cropperToolbarColor") : cropperToolbarColor; + width = options.hasKey("width") ? options.getInt("width") : 0; + height = options.hasKey("height") ? options.getInt("height") : 0; + cropping = options.hasKey("cropping") && options.getBoolean("cropping"); + cropperActiveWidgetColor = options.hasKey("cropperActiveWidgetColor") ? options.getString("cropperActiveWidgetColor") : DEFAULT_TINT; + cropperStatusBarColor = options.hasKey("cropperStatusBarColor") ? options.getString("cropperStatusBarColor") : DEFAULT_TINT; + cropperToolbarColor = options.hasKey("cropperToolbarColor") ? options.getString("cropperToolbarColor") : DEFAULT_TINT; cropperToolbarTitle = options.hasKey("cropperToolbarTitle") ? options.getString("cropperToolbarTitle") : null; - cropperCircleOverlay = options.hasKey("cropperCircleOverlay") ? options.getBoolean("cropperCircleOverlay") : cropperCircleOverlay; - freeStyleCropEnabled = options.hasKey("freeStyleCropEnabled") ? options.getBoolean("freeStyleCropEnabled") : freeStyleCropEnabled; - showCropGuidelines = options.hasKey("showCropGuidelines") ? options.getBoolean("showCropGuidelines") : showCropGuidelines; - hideBottomControls = options.hasKey("hideBottomControls") ? options.getBoolean("hideBottomControls") : hideBottomControls; - enableRotationGesture = options.hasKey("enableRotationGesture") ? options.getBoolean("enableRotationGesture") : enableRotationGesture; - disableCropperColorSetters = options.hasKey("disableCropperColorSetters") ? options.getBoolean("disableCropperColorSetters") : disableCropperColorSetters; + cropperCircleOverlay = options.hasKey("cropperCircleOverlay") && options.getBoolean("cropperCircleOverlay"); + freeStyleCropEnabled = options.hasKey("freeStyleCropEnabled") && options.getBoolean("freeStyleCropEnabled"); + showCropGuidelines = !options.hasKey("showCropGuidelines") || options.getBoolean("showCropGuidelines"); + showCropFrame = !options.hasKey("showCropFrame") || options.getBoolean("showCropFrame"); + hideBottomControls = options.hasKey("hideBottomControls") && options.getBoolean("hideBottomControls"); + enableRotationGesture = options.hasKey("enableRotationGesture") && options.getBoolean("enableRotationGesture"); + disableCropperColorSetters = options.hasKey("disableCropperColorSetters") && options.getBoolean("disableCropperColorSetters"); + useFrontCamera = options.hasKey("useFrontCamera") && options.getBoolean("useFrontCamera"); this.options = options; } @@ -165,10 +173,10 @@ public void clean(final Promise promise) { promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); return; } - - permissionsCheck(activity, promise, Arrays.asList(Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable() { + + permissionsCheck(activity, promise, Collections.singletonList(Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable() { @Override - public Void call() throws Exception { + public Void call() { try { File file = new File(module.getTmpDir(activity)); if (!file.exists()) throw new Exception("File does not exist"); @@ -199,8 +207,8 @@ public void cleanSingle(final String pathToDelete, final Promise promise) { promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); return; } - - permissionsCheck(activity, promise, Arrays.asList(Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable() { + + permissionsCheck(activity, promise, Collections.singletonList(Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable() { @Override public Void call() throws Exception { try { @@ -288,11 +296,11 @@ public void openCamera(final ReadableMap options, final Promise promise) { } setConfiguration(options); - resultCollector = new ResultCollector(promise, multiple); - + resultCollector.setup(promise, false); + permissionsCheck(activity, promise, Arrays.asList(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable() { @Override - public Void call() throws Exception { + public Void call() { initiateCamera(activity); return null; } @@ -312,27 +320,41 @@ private void initiateCamera(Activity activity) { private void initImageCapture(Activity activity) { try { - int requestCode = CAMERA_PICKER_REQUEST; - Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - - File imageFile = createImageFile(); - + String intent; + File dataFile; + + if (mediaType.equals("video")) { + intent = MediaStore.ACTION_VIDEO_CAPTURE; + dataFile = createVideoFile(); + } else { + intent = MediaStore.ACTION_IMAGE_CAPTURE; + dataFile = createImageFile(); + } + + Intent cameraIntent = new Intent(intent); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - mCameraCaptureURI = Uri.fromFile(imageFile); + mCameraCaptureURI = Uri.fromFile(dataFile); } else { mCameraCaptureURI = FileProvider.getUriForFile(activity, - activity.getApplicationContext().getPackageName() + ".provider", - imageFile); + activity.getApplicationContext().getPackageName() + ".provider", + dataFile); } cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, mCameraCaptureURI); - + + if (this.useFrontCamera) { + cameraIntent.putExtra("android.intent.extras.CAMERA_FACING", 1); + cameraIntent.putExtra("android.intent.extras.LENS_FACING_FRONT", 1); + cameraIntent.putExtra("android.intent.extra.USE_FRONT_CAMERA", true); + } + if (cameraIntent.resolveActivity(activity.getPackageManager()) == null) { resultCollector.notifyProblem(E_CANNOT_LAUNCH_CAMERA, "Cannot launch camera"); return; } - - activity.startActivityForResult(cameraIntent, requestCode); + + activity.startActivityForResult(cameraIntent, CAMERA_PICKER_REQUEST); } catch (Exception e) { resultCollector.notifyProblem(E_FAILED_TO_OPEN_CAMERA, e); } @@ -403,11 +425,11 @@ public void openPicker(final ReadableMap options, final Promise promise) { } setConfiguration(options); - resultCollector = new ResultCollector(promise, multiple); - + resultCollector.setup(promise, multiple); + permissionsCheck(activity, promise, Collections.singletonList(Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable() { @Override - public Void call() throws Exception { + public Void call() { initiatePicker(activity); return null; } @@ -424,10 +446,16 @@ public void openCropper(final ReadableMap options, final Promise promise) { } setConfiguration(options); - resultCollector = new ResultCollector(promise, false); - - Uri uri = Uri.parse(options.getString("path")); - startCropping(activity, uri); + resultCollector.setup(promise, false); + + final Uri uri = Uri.parse(options.getString("path")); + permissionsCheck(activity, promise, Collections.singletonList(Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable() { + @Override + public Void call() { + startCropping(activity, uri); + return null; + } + }); } private String getBase64StringFromFile(String absoluteFilePath) { @@ -464,12 +492,12 @@ private String getMimeType(String url) { ContentResolver cr = this.reactContext.getContentResolver(); mimeType = cr.getType(uri); } else { - String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); + String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri + .toString()); if (fileExtension != null) { mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase()); } } - return mimeType; } @@ -478,7 +506,13 @@ private WritableMap getSelection(Activity activity, Uri uri, boolean isCamera) t if (path == null || path.isEmpty()) { throw new Exception("Cannot resolve asset path."); } - + + String mime = getMimeType(path); + if (mime != null && mime.startsWith("video/")) { + getVideo(activity, path, mime); + return null; + } + return getImage(activity, path); } @@ -549,16 +583,15 @@ public void invoke(Object... args) { } }).run(); } - + private String resolveRealPath(Activity activity, Uri uri, boolean isCamera) throws IOException { - String path; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { path = RealPathUtil.getRealPathFromURI(activity, uri); } else { if (isCamera) { - Uri imageUri = Uri.parse(mCurrentPhotoPath); - path = imageUri.getPath(); + Uri mediaUri = Uri.parse(mCurrentMediaPath); + path = mediaUri.getPath(); } else { path = RealPathUtil.getRealPathFromURI(activity, uri); } @@ -588,11 +621,11 @@ private WritableMap getImage(final Activity activity, String path) throws Except if (path.startsWith("http://") || path.startsWith("https://")) { throw new Exception("Cannot select remote files"); } - validateImage(path); - + BitmapFactory.Options original = validateImage(path); + // if compression options are provided image will be compressed. If none options is provided, // then original image will be returned - File compressedImage = compression.compressImage(activity, options, path); + File compressedImage = compression.compressImage(options, path, original); String compressedImagePath = compressedImage.getPath(); BitmapFactory.Options options = validateImage(compressedImagePath); long modificationDate = new File(path).lastModified(); @@ -637,14 +670,15 @@ private void configureCropperColors(UCrop.Options options) { options.setActiveWidgetColor(activeWidgetColor); } } - - private void startCropping(Activity activity, Uri uri) { + + private void startCropping(final Activity activity, final Uri uri) { UCrop.Options options = new UCrop.Options(); options.setCompressionFormat(Bitmap.CompressFormat.JPEG); options.setCompressionQuality(100); options.setCircleDimmedLayer(cropperCircleOverlay); options.setFreeStyleCropEnabled(freeStyleCropEnabled); options.setShowCropGrid(showCropGuidelines); + options.setShowCropFrame(showCropFrame); options.setHideBottomControls(hideBottomControls); if (cropperToolbarTitle != null) { options.setToolbarTitle(cropperToolbarTitle); @@ -660,12 +694,16 @@ private void startCropping(Activity activity, Uri uri) { if (!disableCropperColorSetters) { configureCropperColors(options); } - - UCrop.of(uri, Uri.fromFile(new File(this.getTmpDir(activity), UUID.randomUUID().toString() + ".jpg"))) - .withMaxResultSize(width, height) - .withAspectRatio(width, height) - .withOptions(options) - .start(activity); + + UCrop uCrop = UCrop + .of(uri, Uri.fromFile(new File(this.getTmpDir(activity), UUID.randomUUID().toString() + ".jpg"))) + .withOptions(options); + + if (width > 0 && height > 0) { + uCrop.withAspectRatio(width, height); + } + + uCrop.start(activity); } private void imagePickerResult(Activity activity, final int requestCode, final int resultCode, final Intent data) { @@ -729,7 +767,12 @@ private void cameraPickerResult(Activity activity, final int requestCode, final } else { try { resultCollector.setWaitCount(1); - resultCollector.notifySuccess(getSelection(activity, uri, true)); + WritableMap result = getSelection(activity, uri, true); + + // If recording a video getSelection handles resultCollector part itself and returns null + if (result != null) { + resultCollector.notifySuccess(result); + } } catch (Exception ex) { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, ex.getMessage()); } @@ -759,14 +802,24 @@ private void videoPickerResult(Activity activity, final int requestCode, final i private void croppingResult(Activity activity, final int requestCode, final int resultCode, final Intent data) { if (data != null) { - final Uri resultUri = UCrop.getOutput(data); + Uri resultUri = UCrop.getOutput(data); + if (resultUri != null) { try { + if (width > 0 && height > 0) { + resultUri = Uri.fromFile(compression.resize(resultUri.getPath(), width, height, 100)); + } + WritableMap result = getSelection(activity, resultUri, false); - result.putMap("cropRect", PickerModule.getCroppedRectMap(data)); - - resultCollector.setWaitCount(1); - resultCollector.notifySuccess(result); + + if (result != null) { + result.putMap("cropRect", PickerModule.getCroppedRectMap(data)); + + resultCollector.setWaitCount(1); + resultCollector.notifySuccess(result); + } else { + throw new Exception("Cannot crop video files"); + } } catch (Exception ex) { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, ex.getMessage()); } @@ -818,8 +871,8 @@ private File createImageFile() throws IOException { File image = File.createTempFile(imageFileName, ".jpg", path); // Save a file: path for use with ACTION_VIEW intents - mCurrentPhotoPath = "file:" + image.getAbsolutePath(); - + mCurrentMediaPath = "file:" + image.getAbsolutePath(); + return image; } @@ -836,7 +889,7 @@ private File createVideoFile() throws IOException { File video = File.createTempFile(videoFileName, ".mp4", path); // Save a file: path for use with ACTION_VIEW intents - mCurrentPhotoPath = "file:" + video.getAbsolutePath(); + mCurrentMediaPath = "file:" + video.getAbsolutePath(); return video; } diff --git a/android/src/main/java/com/reactnative/ivpusic/imagepicker/RealPathUtil.java b/android/src/main/java/com/reactnative/ivpusic/imagepicker/RealPathUtil.java index 2803c867b..2a27ea98c 100644 --- a/android/src/main/java/com/reactnative/ivpusic/imagepicker/RealPathUtil.java +++ b/android/src/main/java/com/reactnative/ivpusic/imagepicker/RealPathUtil.java @@ -1,5 +1,6 @@ package com.reactnative.ivpusic.imagepicker; +import android.annotation.TargetApi; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; @@ -17,84 +18,82 @@ import java.io.InputStream; class RealPathUtil { - static String getRealPathFromURI(final Context context, final Uri uri) throws IOException { - - final boolean isKitKat = Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT; - - // DocumentProvider - if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { - // ExternalStorageProvider - if (isExternalStorageDocument(uri)) { - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; - - if ("primary".equalsIgnoreCase(type)) { - return Environment.getExternalStorageDirectory() + "/" + split[1]; - } else { - final int splitIndex = docId.indexOf(':', 1); - final String tag = docId.substring(0, splitIndex); - final String path = docId.substring(splitIndex + 1); - - String nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag); - if (nonPrimaryVolume != null) { - String result = nonPrimaryVolume + "/" + path; - File file = new File(result); - if (file.exists() && file.canRead()) { - return result; + @TargetApi(Build.VERSION_CODES.KITKAT) + static String getRealPathFromURI(final Context context, final Uri uri) throws IOException { + + final boolean isKitKat = Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } else { + final int splitIndex = docId.indexOf(':', 1); + final String tag = docId.substring(0, splitIndex); + final String path = docId.substring(splitIndex + 1); + + String nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag); + if (nonPrimaryVolume != null) { + String result = nonPrimaryVolume + "/" + path; + File file = new File(result); + if (file.exists() && file.canRead()) { + return result; + } + return null; } - return null; } - } - } - // DownloadsProvider - else if (isDownloadsDocument(uri)) { - - final String id = DocumentsContract.getDocumentId(uri); - final Uri contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); - - return getDataColumn(context, contentUri, null, null); - } - // MediaProvider - else if (isMediaDocument(uri)) { - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; - - Uri contentUri = null; - if ("image".equals(type)) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - } else if ("video".equals(type)) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - } else if ("audio".equals(type)) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - } - - final String selection = "_id=?"; - final String[] selectionArgs = new String[] { - split[1] - }; - - return getDataColumn(context, contentUri, selection, selectionArgs); - } - } - // MediaStore (and general) - else if ("content".equalsIgnoreCase(uri.getScheme())) { - - // Return the remote address - if (isGooglePhotosUri(uri)) - return uri.getLastPathSegment(); - - return getDataColumn(context, uri, null, null); - } - // File - else if ("file".equalsIgnoreCase(uri.getScheme())) { - return uri.getPath(); - } - - return null; - } + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[] { + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } /** * If an image/video has been selected from a cloud storage, this method @@ -108,6 +107,7 @@ else if ("file".equalsIgnoreCase(uri.getScheme())) { private static File writeToFile(Context context, String fileName, Uri uri) { String tmpDir = context.getCacheDir() + "/react-native-image-crop-picker"; Boolean created = new File(tmpDir).mkdir(); + fileName = fileName.substring(fileName.lastIndexOf('/') + 1); File path = new File(tmpDir); File file = new File(path, fileName); try { @@ -127,106 +127,102 @@ private static File writeToFile(Context context, String fileName, Uri uri) { return file; } - /** - * Get the value of the data column for this Uri. This is useful for - * MediaStore Uris, and other file-based ContentProviders. - * - * @param context The context. - * @param uri The Uri to query. - * @param selection (Optional) Filter used in the query. - * @param selectionArgs (Optional) Selection arguments used in the query. - * @return The value of the _data column, which is typically a file path. - */ - private static String getDataColumn(Context context, Uri uri, String selection, - String[] selectionArgs) { - - Cursor cursor = null; - final String[] projection = { - MediaStore.MediaColumns.DATA, - MediaStore.MediaColumns.DISPLAY_NAME, - MediaStore.MediaColumns.MIME_TYPE - }; - - try { - cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, - null); - if (cursor != null && cursor.moveToFirst()) { - final int index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA); - Log.d("CROP_PICKER", String.valueOf(index)); - Log.d("CROP_PICKER", "VALUE -> " + cursor.getString(index)); - String path = cursor.getString(index); - - if (path != null) { - return cursor.getString(index); - } else { + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + final String[] projection = { + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.MIME_TYPE + }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + // Fall back to writing to file if _data column does not exist + final int index = cursor.getColumnIndex(MediaStore.MediaColumns.DATA); + String path = index > -1 ? cursor.getString(index) : null; + if (path != null) { + return cursor.getString(index); + } else { final MimeTypeMap mime = MimeTypeMap.getSingleton(); final int indexDisplayName = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME); final int indexMimeType = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE); String fileName = cursor.getString(indexDisplayName); String extension = mime.getExtensionFromMimeType(cursor.getString(indexMimeType)); - Log.d("CROP_PICKER", "File name: " + fileName); - Log.d("CROP_PICKER", "File extension: " + extension); File fileWritten = writeToFile(context, fileName + "." + extension, uri); - Log.d("CROP_PICKER", "File path: " + fileWritten.getAbsolutePath()); return fileWritten.getAbsolutePath(); + } + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private static String getPathToNonPrimaryVolume(Context context, String tag) { + File[] volumes = context.getExternalCacheDirs(); + if (volumes != null) { + for (File volume : volumes) { + if (volume != null) { + String path = volume.getAbsolutePath(); + if (path != null) { + int index = path.indexOf(tag); + if (index != -1) { + return path.substring(0, index) + tag; + } + } + } } - } - } finally { - if (cursor != null) - cursor.close(); - } - return null; - } - - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is ExternalStorageProvider. - */ - private static boolean isExternalStorageDocument(Uri uri) { - return "com.android.externalstorage.documents".equals(uri.getAuthority()); - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is DownloadsProvider. - */ - private static boolean isDownloadsDocument(Uri uri) { - return "com.android.providers.downloads.documents".equals(uri.getAuthority()); - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is MediaProvider. - */ - private static boolean isMediaDocument(Uri uri) { - return "com.android.providers.media.documents".equals(uri.getAuthority()); - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is Google Photos. - */ - private static boolean isGooglePhotosUri(Uri uri) { - return "com.google.android.apps.photos.content".equals(uri.getAuthority()); - } - - private static String getPathToNonPrimaryVolume(Context context, String tag) { - File[] volumes = context.getExternalCacheDirs(); - if (volumes != null) { - for (File volume : volumes) { - if (volume != null) { - String path = volume.getAbsolutePath(); - if (path != null) { - int index = path.indexOf(tag); - if (index != -1) { - return path.substring(0, index) + tag; - } - } - } - } - } - return null; - } + } + return null; + } } diff --git a/android/src/main/java/com/reactnative/ivpusic/imagepicker/ResultCollector.java b/android/src/main/java/com/reactnative/ivpusic/imagepicker/ResultCollector.java index dbfc6fff0..8bd1b604f 100644 --- a/android/src/main/java/com/reactnative/ivpusic/imagepicker/ResultCollector.java +++ b/android/src/main/java/com/reactnative/ivpusic/imagepicker/ResultCollector.java @@ -19,12 +19,16 @@ class ResultCollector { private boolean multiple; private AtomicInteger waitCounter; private WritableArray arrayResult; - private boolean resultSent = false; + private boolean resultSent; - ResultCollector(Promise promise, boolean multiple) { + synchronized void setup(Promise promise, boolean multiple) { this.promise = promise; this.multiple = multiple; + this.resultSent = false; + this.waitCount = 0; + this.waitCounter = new AtomicInteger(0); + if (multiple) { this.arrayResult = new WritableNativeArray(); } @@ -32,14 +36,28 @@ class ResultCollector { // if user has provided "multiple" option, we will wait for X number of result to come, // and also return result as an array - void setWaitCount(int waitCount) { + synchronized void setWaitCount(int waitCount) { this.waitCount = waitCount; this.waitCounter = new AtomicInteger(0); } - synchronized void notifySuccess(WritableMap result) { + synchronized private boolean isRequestValid() { if (resultSent) { Log.w("image-crop-picker", "Skipping result, already sent..."); + return false; + } + + if (promise == null) { + Log.w("image-crop-picker", "Trying to notify success but promise is not set"); + return false; + } + + return true; + } + + synchronized void notifySuccess(WritableMap result) { + if (!isRequestValid()) { + return; } if (multiple) { @@ -57,8 +75,8 @@ synchronized void notifySuccess(WritableMap result) { } synchronized void notifyProblem(String code, String message) { - if (resultSent) { - Log.w("image-crop-picker", "Skipping result, already sent..."); + if (!isRequestValid()) { + return; } Log.e("image-crop-picker", "Promise rejected. " + message); @@ -67,8 +85,8 @@ synchronized void notifyProblem(String code, String message) { } synchronized void notifyProblem(String code, Throwable throwable) { - if (resultSent) { - Log.w("image-crop-picker", "Skipping result, already sent..."); + if (!isRequestValid()) { + return; } Log.e("image-crop-picker", "Promise rejected. " + throwable.getMessage()); diff --git a/example/README.md b/example/README.md new file mode 100644 index 000000000..bf13df74f --- /dev/null +++ b/example/README.md @@ -0,0 +1,22 @@ +# Example project + +## Install deps + +```bash +cd example +yarn install +``` + +## Running (ios) + +```bash +cd example +yarn ios +``` + +## Running (android) + +```bash +cd example +yarn android +``` \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 3a58c5822..8b1ef31f0 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -18,6 +18,9 @@ import com.android.build.OutputFile * // the entry file for bundle generation * entryFile: "index.android.js", * + * // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format + * bundleCommand: "ram-bundle", + * * // whether to bundle JS and assets in debug mode * bundleInDebug: false, * @@ -33,6 +36,13 @@ import com.android.build.OutputFile * // bundleInPaidRelease: true, * // bundleInBeta: true, * + * // whether to disable dev mode in custom build variants (by default only disabled in release) + * // for example: to disable dev mode in the staging build type (if configured) + * devDisabledInStaging: true, + * // The configuration property can be in the following formats + * // 'devDisabledIn${productFlavor}${buildType}' + * // 'devDisabledIn${buildType}' + * * // the root of your project, i.e. where "package.json" lives * root: "../../", * @@ -58,13 +68,18 @@ import com.android.build.OutputFile * inputExcludes: ["android/**", "ios/**"], * * // override which node gets called and with what additional arguments - * nodeExecutableAndArgs: ["node"] + * nodeExecutableAndArgs: ["node"], * * // supply additional arguments to the packager * extraPackagerArgs: [] * ] */ +project.ext.react = [ + entryFile : "index.js", + enableHermes: false, // clean and rebuild if changing +] + apply from: "../../node_modules/react-native/react.gradle" /** @@ -82,18 +97,45 @@ def enableSeparateBuildPerCPUArchitecture = false */ def enableProguardInReleaseBuilds = false +/** + * The preferred build flavor of JavaScriptCore. + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'org.webkit:android-jsc:+' + +/** + * Whether to enable the Hermes VM. + * + * This should be set on project.ext.react and mirrored here. If it is not set + * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode + * and the benefits of using Hermes will therefore be sharply reduced. + */ +def enableHermes = project.ext.react.get("enableHermes", false) + android { - compileSdkVersion 27 - buildToolsVersion "27.0.0" + compileSdkVersion rootProject.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } defaultConfig { applicationId "com.example" - minSdkVersion 18 - targetSdkVersion 27 + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" + ndk { - abiFilters "armeabi-v7a", "x86" + abiFilters 'armeabi-v7a', 'x86' } } splits { @@ -101,11 +143,15 @@ android { reset() enable enableSeparateBuildPerCPUArchitecture universalApk false // If true, also generate a universal APK - include "armeabi-v7a", "x86" + include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" } } buildTypes { + debug { + } release { + // Caution! In production, you need to generate your own keystore file. + // see https://facebook.github.io/react-native/docs/signed-apk-android. minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } @@ -114,23 +160,38 @@ android { applicationVariants.all { variant -> variant.outputs.each { output -> // For each separate APK per architecture, set a unique version code as described here: - // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits - def versionCodes = ["armeabi-v7a":1, "x86":2] + // https://developer.android.com/studio/build/configure-apk-splits.html + def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] def abi = output.getFilter(OutputFile.ABI) if (abi != null) { // null for the universal-debug, universal-release variants output.versionCodeOverride = versionCodes.get(abi) * 1048576 + defaultConfig.versionCode } + } } + + packagingOptions { + pickFirst '**/armeabi-v7a/libc++_shared.so' + pickFirst '**/x86/libc++_shared.so' + pickFirst '**/arm64-v8a/libc++_shared.so' + pickFirst '**/x86_64/libc++_shared.so' + pickFirst '**/x86/libjsc.so' + pickFirst '**/armeabi-v7a/libjsc.so' + } } dependencies { - compile project(':react-native-video') - compile project(':react-native-image-crop-picker') - compile fileTree(dir: "libs", include: ["*.jar"]) - compile "com.android.support:appcompat-v7:27.0.2" - compile "com.facebook.react:react-native:+" + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "com.facebook.react:react-native:+" // From node_modules + + if (enableHermes) { + def hermesPath = "../../node_modules/hermesvm/android/"; + debugImplementation files(hermesPath + "hermes-debug.aar") + releaseImplementation files(hermesPath + "hermes-release.aar") + } else { + implementation jscFlavor + } } // Run this once to be able to run the application with BUCK @@ -138,4 +199,6 @@ dependencies { task copyDownloadableDepsToLibs(type: Copy) { from configurations.compile into 'libs' -} \ No newline at end of file +} + +apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index b7e16423c..d6c2b854e 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,32 +1,26 @@ + package="com.example"> - + - - - - - - - - - + android:theme="@style/AppTheme"> + + + + + + + - diff --git a/example/android/app/src/main/java/com/example/MainActivity.java b/example/android/app/src/main/java/com/example/MainActivity.java index e84b72551..877a3363d 100644 --- a/example/android/app/src/main/java/com/example/MainActivity.java +++ b/example/android/app/src/main/java/com/example/MainActivity.java @@ -1,6 +1,9 @@ package com.example; +import android.os.Bundle; + import com.facebook.react.ReactActivity; +import com.facebook.soloader.SoLoader; public class MainActivity extends ReactActivity { @@ -12,4 +15,10 @@ public class MainActivity extends ReactActivity { protected String getMainComponentName() { return "example"; } + + @Override + protected void onCreate(Bundle savedInstanceState) { + SoLoader.init(this, /* native exopackage */ false); + super.onCreate(savedInstanceState); + } } diff --git a/example/android/app/src/main/java/com/example/MainApplication.java b/example/android/app/src/main/java/com/example/MainApplication.java index 25c4e20d1..0bf210e14 100644 --- a/example/android/app/src/main/java/com/example/MainApplication.java +++ b/example/android/app/src/main/java/com/example/MainApplication.java @@ -2,14 +2,11 @@ import android.app.Application; +import com.facebook.react.PackageList; import com.facebook.react.ReactApplication; -import com.brentvatne.react.ReactVideoPackage; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; -import com.facebook.react.shell.MainReactPackage; -import com.reactnative.ivpusic.imagepicker.PickerPackage; -import java.util.Arrays; import java.util.List; public class MainApplication extends Application implements ReactApplication { @@ -22,11 +19,11 @@ public boolean getUseDeveloperSupport() { @Override protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), - new ReactVideoPackage(), - new PickerPackage() - ); + @SuppressWarnings("UnnecessaryLocalVariable") + List packages = new PackageList(this).getPackages(); + // Packages that cannot be autolinked yet can be added manually here, for example: + // packages.add(new MyReactNativePackage()); + return packages; } }; diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index cde69bccc..3f7c244b1 100644 Binary files a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index c133a0cbd..5a6ea37e2 100644 Binary files a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index bfa42f0e7..424e38916 100644 Binary files a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 324e72cdd..efc21941d 100644 Binary files a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/build.gradle b/example/android/build.gradle index d4a1b452e..57acf829a 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,15 +1,23 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext { + buildToolsVersion = "28.0.3" + minSdkVersion = 16 + compileSdkVersion = 28 + targetSdkVersion = 28 + supportLibVersion = "28.0.0" + } repositories { jcenter() maven { url 'https://maven.google.com/' name 'Google' } + google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.4.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -21,6 +29,10 @@ allprojects { mavenLocal() jcenter() maven { url "$rootDir/../node_modules/react-native/android" } + maven { + // Android JSC is installed from npm + url("$rootDir/../node_modules/jsc-android/dist") + } maven { url 'https://maven.google.com' } maven { url "https://jitpack.io" } maven { diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 1fd964e90..812ac84eb 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -18,3 +18,6 @@ # org.gradle.parallel=true android.useDeprecatedNdk=true + +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 11b016f6e..53f6c2194 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Dec 07 01:53:01 CET 2017 +#Fri May 10 14:06:26 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 2aa498d65..ab26400bc 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,8 +1,5 @@ rootProject.name = 'example' -include ':app' -include ':react-native-video' -project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android') +apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) -include ':react-native-image-crop-picker' -project(':react-native-image-crop-picker').projectDir = new File(settingsDir, '../../android') \ No newline at end of file +include ':app' \ No newline at end of file diff --git a/example/app.js b/example/app.js index 98534cbe7..a5908fda8 100644 --- a/example/app.js +++ b/example/app.js @@ -35,16 +35,17 @@ export default class App extends Component { }; } - pickSingleWithCamera(cropping) { + pickSingleWithCamera(cropping, mediaType='photo') { ImagePicker.openCamera({ cropping: cropping, width: 500, height: 500, includeExif: true, + mediaType, }).then(image => { console.log('received image', image); this.setState({ - image: {uri: image.path, width: image.width, height: image.height}, + image: {uri: image.path, width: image.width, height: image.height, mime: image.mime}, images: null }); }).catch(e => alert(e)); @@ -106,15 +107,15 @@ export default class App extends Component { }); } - pickSingle(cropit, circular=false) { + pickSingle(cropit, circular=false, mediaType) { ImagePicker.openPicker({ - width: 300, - height: 300, + width: 500, + height: 500, cropping: cropit, cropperCircleOverlay: circular, - compressImageMaxWidth: 640, - compressImageMaxHeight: 480, - compressImageQuality: 0.5, + compressImageMaxWidth: 1000, + compressImageMaxHeight: 1000, + compressImageQuality: 1, compressVideoPreset: 'MediumQuality', includeExif: true, }).then(image => { @@ -134,6 +135,7 @@ export default class App extends Component { multiple: true, waitAnimationEnd: false, includeExif: true, + forceJpg: true, }).then(images => { this.setState({ image: null, @@ -150,6 +152,7 @@ export default class App extends Component { } renderVideo(video) { + console.log('rendering video'); return (