From 7eaa65dc6dded4eb62591866bc9167c2fd578fdf Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 5 Mar 2017 03:52:57 +0700 Subject: [PATCH] [ANDROID] Refactoring of Code plus bug fixes (#496) * Fixed issue#485 Fixed issue#478 * Removed useless file * Recoverd options for launchCamera and launchImageLibrary --- .gitignore | 1 + .npmignore | 1 + Example/android/app/build.gradle | 2 +- .../java/com/example/MainApplication.java | 2 +- Example/android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- Example/package.json | 4 +- README.md | 86 ++++- android/build.gradle | 8 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../com/imagepicker/ImagePickerModule.java | 361 ++++++++++++------ .../com/imagepicker/ImagePickerPackage.java | 17 +- .../java/com/imagepicker/ResponseHelper.java | 78 ++++ .../OnImagePickerPermissionsCallback.java | 12 + .../permissions/PermissionUtils.java | 74 ++++ .../permissions/PermissionsHelper.java | 11 + .../com/imagepicker/utils/ButtonsHelper.java | 10 - .../main/java/com/imagepicker/utils/UI.java | 39 +- android/src/main/res/layout/list_item.xml | 15 + android/src/main/res/values/themes.xml | 11 + index.js | 9 +- package.json | 7 +- 22 files changed, 606 insertions(+), 148 deletions(-) create mode 100644 android/src/main/java/com/imagepicker/ResponseHelper.java create mode 100644 android/src/main/java/com/imagepicker/permissions/OnImagePickerPermissionsCallback.java create mode 100644 android/src/main/java/com/imagepicker/permissions/PermissionUtils.java create mode 100644 android/src/main/java/com/imagepicker/permissions/PermissionsHelper.java create mode 100644 android/src/main/res/layout/list_item.xml create mode 100644 android/src/main/res/values/themes.xml diff --git a/.gitignore b/.gitignore index 83b85db7d..e58ac8608 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ xcuserdata/ ### Node node_modules *.log +yarn.lock ## Android iml *.iml diff --git a/.npmignore b/.npmignore index 4fe9b51ec..b80bafd27 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,3 @@ Example/ images/ +node_modules/ diff --git a/Example/android/app/build.gradle b/Example/android/app/build.gradle index c6d296658..7fcc93e06 100644 --- a/Example/android/app/build.gradle +++ b/Example/android/app/build.gradle @@ -83,7 +83,7 @@ android { defaultConfig { applicationId "com.example" minSdkVersion 16 - targetSdkVersion 23 + targetSdkVersion 22 versionCode 1 versionName "1.0" ndk { 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 4395277b8..74fe278a4 100644 --- a/Example/android/app/src/main/java/com/example/MainApplication.java +++ b/Example/android/app/src/main/java/com/example/MainApplication.java @@ -18,7 +18,7 @@ public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override - protected boolean getUseDeveloperSupport() { + public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } diff --git a/Example/android/build.gradle b/Example/android/build.gradle index 46047bdad..7b46d23f4 100644 --- a/Example/android/build.gradle +++ b/Example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.0' + classpath 'com.android.tools.build:gradle:2.2.+' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/Example/android/gradle/wrapper/gradle-wrapper.properties b/Example/android/gradle/wrapper/gradle-wrapper.properties index 7272975b9..5a4aec38b 100644 --- a/Example/android/gradle/wrapper/gradle-wrapper.properties +++ b/Example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/Example/package.json b/Example/package.json index d2bfa3c93..6b8e35cec 100644 --- a/Example/package.json +++ b/Example/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "react": "15.4.1", - "react-native": "0.40.0", - "react-native-image-picker": "../" + "react-native": "^0.42.0", + "react-native-image-picker": "file:../" } } diff --git a/README.md b/README.md index d75866c70..f092fbece 100644 --- a/README.md +++ b/README.md @@ -46,20 +46,37 @@ IMPORTANT NOTE: You'll still need to perform step 4 for iOS and step 3 for Andro include ':react-native-image-picker' project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android') ``` -2. Add the compile line to the dependencies in `android/app/build.gradle`: +2. Update the android build tools version to `2.2.+` in `android/build.gradle`: + ```gradle + buildscript { + ... + dependencies { + classpath 'com.android.tools.build:gradle:2.2.+' // <- USE 2.2.+ version + } + ... + } + ... + ``` +3. Update the gradle version to `2.14.1` in `android/gradle/wrapper/gradle-wrapper.properties`: + ``` + ... + distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip + ``` + +4. Add the compile line to the dependencies in `android/app/build.gradle`: ```gradle dependencies { compile project(':react-native-image-picker') } ``` -3. Add the required permissions in `AndroidManifest.xml`: +5. Add the required permissions in `AndroidManifest.xml`: ```xml ``` -4. Add the import and link the package in `MainApplication.java`: +6. Add the import and link the package in `MainApplication.java`: ```java import com.imagepicker.ImagePickerPackage; // <-- add this import @@ -70,10 +87,67 @@ IMPORTANT NOTE: You'll still need to perform step 4 for iOS and step 3 for Andro return Arrays.asList( new MainReactPackage(), new ImagePickerPackage() // <-- add this line + // OR if you want to customize dialog style + new ImagePickerPackage(R.style.my_dialog_style) ); } } -``` + ``` + + Customization settings of dialog `android/app/res/values/themes.xml`: + + ```xml + + + + + ``` +##### Android (Optional) + +If `MainActivity` is not instance of `ReactActivity`, you will need to implement `OnImagePickerPermissionsCallback` to `MainActivity`: + + ```java + import com.imagepicker.permissions.OnImagePickerPermissionsCallback; // <- add this import + import com.facebook.react.modules.core.PermissionListener; // <- add this import + + public class MainActivity extends YourActivity implements OnImagePickerPermissionsCallback { + private PermissionListener listener; // <- add this attribute + + // Your methods here + + // Copy from here + + @Override + public void setPermissionListener(PermissionListener listener) + { + this.listener = listener; + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) + { + if (listener != null) + { + listener.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + // To here + } + ``` +This code allows to pass result of request permissions to native part. + + ## Usage @@ -169,6 +243,10 @@ storageOptions.skipBackup | OK | - | If true, the photo will NOT be backed up to storageOptions.path | OK | - | If set, will save image at /Documents/[path] rather than the root storageOptions.cameraRoll | OK | - | If true, the cropped photo will be saved to the iOS Camera Roll. storageOptions.waitUntilSaved | OK | - | If true, will delay the response callback until after the photo/video was saved to the Camera Roll. If the photo or video was just taken, then the file name and timestamp fields are only provided in the response object when this is true. +permissionDenied.title | - | OK | Title of explaining permissions dialog. By default `Permission denied`. +permissionDenied.text | - | OK | Message of explaining permissions dialog. By default `To be able to take pictures with your camera and choose images from your library.`. +permissionDenied.reTryTitle | - | OK | Title of re-try button. By default `re-try` +permissionDenied.okTitle | - | OK | Title of ok button. By default `I'm sure` ### The Response Object diff --git a/android/build.gradle b/android/build.gradle index 57b20e394..e653cae43 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -13,7 +13,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:1.3.1' + classpath 'com.android.tools.build:gradle:2.2.+' } } @@ -36,6 +36,12 @@ android { repositories { mavenCentral() + maven { + url "$projectDir/../Example/node_modules/react-native/android" + } + maven { + url "$projectDir/../../react-native/android" + } } dependencies { diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index a6d5f24a0..04e285f34 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/android/src/main/java/com/imagepicker/ImagePickerModule.java b/android/src/main/java/com/imagepicker/ImagePickerModule.java index 8ce8cd9cb..30bea72b3 100644 --- a/android/src/main/java/com/imagepicker/ImagePickerModule.java +++ b/android/src/main/java/com/imagepicker/ImagePickerModule.java @@ -4,6 +4,7 @@ import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -14,23 +15,28 @@ import android.os.Build; import android.os.Environment; import android.provider.MediaStore; +import android.provider.Settings; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StyleRes; import android.support.v4.app.ActivityCompat; import android.support.v4.content.FileProvider; +import android.support.v7.app.AlertDialog; import android.util.Base64; import android.util.Log; import android.webkit.MimeTypeMap; import android.content.pm.PackageManager; import android.media.MediaScannerConnection; +import com.facebook.react.ReactActivity; import com.facebook.react.bridge.ActivityEventListener; -import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableMap; +import com.imagepicker.permissions.PermissionUtils; +import com.imagepicker.permissions.OnImagePickerPermissionsCallback; import com.imagepicker.utils.RealPathUtil; import com.imagepicker.utils.UI; @@ -42,6 +48,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URL; import java.text.DateFormat; @@ -51,18 +58,25 @@ import java.util.Locale; import java.util.TimeZone; import java.util.UUID; +import com.facebook.react.modules.core.PermissionListener; -public class ImagePickerModule extends ReactContextBaseJavaModule implements ActivityEventListener { +public class ImagePickerModule extends ReactContextBaseJavaModule + implements ActivityEventListener +{ - static final int REQUEST_LAUNCH_IMAGE_CAPTURE = 13001; - static final int REQUEST_LAUNCH_IMAGE_LIBRARY = 13002; - static final int REQUEST_LAUNCH_VIDEO_LIBRARY = 13003; - static final int REQUEST_LAUNCH_VIDEO_CAPTURE = 13004; + static final int REQUEST_LAUNCH_IMAGE_CAPTURE = 13001; + static final int REQUEST_LAUNCH_IMAGE_LIBRARY = 13002; + static final int REQUEST_LAUNCH_VIDEO_LIBRARY = 13003; + static final int REQUEST_LAUNCH_VIDEO_CAPTURE = 13004; + static final int REQUEST_PERMISSIONS_FOR_CAMERA = 14001; + static final int REQUEST_PERMISSIONS_FOR_LIBRARY = 14002; private final ReactApplicationContext reactContext; + private final int dialogThemeId; - private Uri cameraCaptureURI; private Callback callback; + private ReadableMap options; + private Uri cameraCaptureURI; private Boolean noData = false; private Boolean pickVideo = false; private int maxWidth = 0; @@ -71,14 +85,54 @@ public class ImagePickerModule extends ReactContextBaseJavaModule implements Act private int rotation = 0; private int videoQuality = 1; private int videoDurationLimit = 0; - WritableMap response; + private ResponseHelper responseHelper = new ResponseHelper(); + private PermissionListener listener = new PermissionListener() + { + public boolean onRequestPermissionsResult(final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) + { + boolean permissionsGranted = true; + for (int i = 0; i < permissions.length; i++) + { + final boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED; + permissionsGranted = permissionsGranted && granted; + } + + if (callback == null || options == null) + { + return false; + } + + if (!permissionsGranted) + { + responseHelper.invokeError(callback, "Permissions weren't granted"); + return false; + } + + switch (requestCode) + { + case REQUEST_PERMISSIONS_FOR_CAMERA: + launchCamera(options, callback); + break; + + case REQUEST_PERMISSIONS_FOR_LIBRARY: + launchImageLibrary(options, callback); + break; - public ImagePickerModule(ReactApplicationContext reactContext) { + } + return true; + } + }; + + public ImagePickerModule(ReactApplicationContext reactContext, + @StyleRes final int dialogThemeId) + { super(reactContext); + this.dialogThemeId = dialogThemeId; this.reactContext = reactContext; - - reactContext.addActivityEventListener(this); + this.reactContext.addActivityEventListener(this); } @Override @@ -92,13 +146,14 @@ public void showImagePicker(final ReadableMap options, final Callback callback) if (currentActivity == null) { - cleanResponse(); - response.putString("error", "can't find current Activity"); - callback.invoke(response); + responseHelper.invokeError(callback, "can't find current Activity"); return; } - UI.showDialog(currentActivity, options, new UI.OnAction() + this.callback = callback; + this.options = options; + + final AlertDialog dialog = UI.chooseDialog(this, options, new UI.OnAction() { @Override public void onTakePhoto() @@ -115,52 +170,45 @@ public void onUseLibrary() @Override public void onCancel() { - cleanResponse(); - response.putBoolean("didCancel", true); - callback.invoke(response); + doOnCancel(); } @Override public void onCustomButton(@NonNull final String action) { - cleanResponse(); - response.putString("customButton", action); - callback.invoke(response); - } - - @Override - public void onDialogWasCanceled(@NonNull final String action) - { - cleanResponse(); - response.putBoolean(action, true); - callback.invoke(response); + responseHelper.invokeCustomButton(callback, action); } }); + dialog.show(); + } + + public void doOnCancel() + { + responseHelper.invokeCancel(callback); } // NOTE: Currently not reentrant / doesn't support concurrent requests @ReactMethod public void launchCamera(final ReadableMap options, final Callback callback) { - response = Arguments.createMap(); - if (!isCameraAvailable()) { - response.putString("error", "Camera not available"); - callback.invoke(response); + responseHelper.invokeError(callback, "Camera not available"); return; } - Activity currentActivity = getCurrentActivity(); + final Activity currentActivity = getCurrentActivity(); if (currentActivity == null) { - response.putString("error", "can't find current Activity"); - callback.invoke(response); + responseHelper.invokeError(callback, "can't find current Activity"); return; } - if (!permissionsCheck(currentActivity)) { + this.options = options; + + if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_CAMERA)) + { return; } - parseOptions(options); + parseOptions(this.options); int requestCode; Intent cameraIntent; @@ -178,12 +226,15 @@ public void launchCamera(final ReadableMap options, final Callback callback) { // we create a tmp file to save the result File imageFile = createNewFile(); cameraCaptureURI = compatUriFromFile(reactContext, imageFile); + if (cameraCaptureURI == null) { + responseHelper.invokeError(callback, "Couldn't get file path for photo"); + return; + } cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraCaptureURI); } if (cameraIntent.resolveActivity(reactContext.getPackageManager()) == null) { - response.putString("error", "Cannot launch camera"); - callback.invoke(response); + responseHelper.invokeError(callback, "Cannot launch camera"); return; } @@ -193,29 +244,28 @@ public void launchCamera(final ReadableMap options, final Callback callback) { currentActivity.startActivityForResult(cameraIntent, requestCode); } catch (ActivityNotFoundException e) { e.printStackTrace(); - response = Arguments.createMap(); - response.putString("error", "Cannot launch camera"); - callback.invoke(response); + responseHelper.invokeError(callback, "Cannot launch camera"); } } // NOTE: Currently not reentrant / doesn't support concurrent requests @ReactMethod - public void launchImageLibrary(final ReadableMap options, final Callback callback) { - response = Arguments.createMap(); - - Activity currentActivity = getCurrentActivity(); + public void launchImageLibrary(final ReadableMap options, final Callback callback) + { + final Activity currentActivity = getCurrentActivity(); if (currentActivity == null) { - response.putString("error", "can't find current Activity"); - callback.invoke(response); + responseHelper.invokeError(callback, "can't find current Activity"); return; } - if (!permissionsCheck(currentActivity)) { + this.options = options; + + if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_LIBRARY)) + { return; } - parseOptions(options); + parseOptions(this.options); int requestCode; Intent libraryIntent; @@ -230,8 +280,7 @@ public void launchImageLibrary(final ReadableMap options, final Callback callbac } if (libraryIntent.resolveActivity(reactContext.getPackageManager()) == null) { - response.putString("error", "Cannot launch photo library"); - callback.invoke(response); + responseHelper.invokeError(callback, "Cannot launch photo library"); return; } @@ -241,11 +290,11 @@ public void launchImageLibrary(final ReadableMap options, final Callback callbac currentActivity.startActivityForResult(libraryIntent, requestCode); } catch (ActivityNotFoundException e) { e.printStackTrace(); - response.putString("error", "Cannot launch photo library"); - callback.invoke(response); + responseHelper.invokeError(callback, "Cannot launch photo library"); } } + @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { //robustness code if (callback == null || (cameraCaptureURI == null && requestCode == REQUEST_LAUNCH_IMAGE_CAPTURE) @@ -254,12 +303,11 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, return; } - response = Arguments.createMap(); + responseHelper.cleanResponse(); // user cancel if (resultCode != Activity.RESULT_OK) { - response.putBoolean("didCancel", true); - callback.invoke(response); + responseHelper.invokeResponse(callback); callback = null; return; } @@ -274,16 +322,17 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, uri = data.getData(); break; case REQUEST_LAUNCH_VIDEO_LIBRARY: - response.putString("uri", data.getData().toString()); - response.putString("path", getRealPathFromURI(data.getData())); - callback.invoke(response); + responseHelper.putString("uri", data.getData().toString()); + responseHelper.putString("path", getRealPathFromURI(data.getData())); + responseHelper.invokeResponse(callback); callback = null; return; case REQUEST_LAUNCH_VIDEO_CAPTURE: - response.putString("uri", data.getData().toString()); - response.putString("path", getRealPathFromURI(data.getData())); - this.fileScan(response.getString("path")); - callback.invoke(response); + final String path = getRealPathFromURI(data.getData()); + responseHelper.putString("uri", data.getData().toString()); + responseHelper.putString("path", path); + this.fileScan(path); + responseHelper.invokeResponse(callback); callback = null; return; default: @@ -310,9 +359,9 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, uri = Uri.fromFile(file); } catch (Exception e) { // image not in cache - response.putString("error", "Could not read photo"); - response.putString("uri", uri.toString()); - callback.invoke(response); + responseHelper.putString("error", "Could not read photo"); + responseHelper.putString("uri", uri.toString()); + responseHelper.invokeResponse(callback); callback = null; return; } @@ -328,19 +377,20 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, float latitude = latlng[0]; float longitude = latlng[1]; if(latitude != 0f || longitude != 0f) { - response.putDouble("latitude", latitude); - response.putDouble("longitude", longitude); + responseHelper.putDouble("latitude", latitude); + responseHelper.putDouble("longitude", longitude); } - String timestamp = exif.getAttribute(ExifInterface.TAG_DATETIME); - SimpleDateFormat exifDatetimeFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); + final String timestamp = exif.getAttribute(ExifInterface.TAG_DATETIME); + final SimpleDateFormat exifDatetimeFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); - DateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + final DateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); isoFormat.setTimeZone(TimeZone.getTimeZone("UTC")); try { - String isoFormatString = isoFormat.format(exifDatetimeFormat.parse(timestamp)) + "Z"; - response.putString("timestamp", isoFormatString); + final String isoFormatString = new StringBuilder(isoFormat.format(exifDatetimeFormat.parse(timestamp))) + .append("Z").toString(); + responseHelper.putString("timestamp", isoFormatString); } catch (Exception e) {} int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); @@ -358,12 +408,11 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, currentRotation = 180; break; } - response.putInt("originalRotation", currentRotation); - response.putBoolean("isVertical", isVertical); + responseHelper.putInt("originalRotation", currentRotation); + responseHelper.putBoolean("isVertical", isVertical); } catch (IOException e) { e.printStackTrace(); - response.putString("error", e.getMessage()); - callback.invoke(response); + responseHelper.invokeError(callback, e.getMessage()); callback = null; return; } @@ -376,32 +425,33 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, // don't create a new file if contraint are respected if (((initialWidth < maxWidth && maxWidth > 0) || maxWidth == 0) && ((initialHeight < maxHeight && maxHeight > 0) || maxHeight == 0) && quality == 100 && (rotation == 0 || currentRotation == rotation)) { - response.putInt("width", initialWidth); - response.putInt("height", initialHeight); + responseHelper.putInt("width", initialWidth); + responseHelper.putInt("height", initialHeight); } else { File resized = getResizedImage(realPath, initialWidth, initialHeight); if (resized == null) { - response.putString("error", "Can't resize the image"); + responseHelper.putString("error", "Can't resize the image"); } else { realPath = resized.getAbsolutePath(); uri = Uri.fromFile(resized); BitmapFactory.decodeFile(realPath, options); - response.putInt("width", options.outWidth); - response.putInt("height", options.outHeight); + responseHelper.putInt("width", options.outWidth); + responseHelper.putInt("height", options.outHeight); } } - response.putString("uri", uri.toString()); - response.putString("path", realPath); + responseHelper.putString("uri", uri.toString()); + responseHelper.putString("path", realPath); if (!noData) { - response.putString("data", getBase64StringFromFile(realPath)); + responseHelper.putString("data", getBase64StringFromFile(realPath)); } - putExtraFileInfo(realPath, response); + putExtraFileInfo(realPath, responseHelper); - callback.invoke(response); + responseHelper.invokeResponse(callback); callback = null; + this.options = null; } /** @@ -440,16 +490,85 @@ private static long parseTimestamp(String dateTimeString, String subSecs) { } } - private boolean permissionsCheck(Activity activity) { - int writePermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); - int cameraPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.CAMERA); - if (writePermission != PackageManager.PERMISSION_GRANTED || cameraPermission != PackageManager.PERMISSION_GRANTED) { - String[] PERMISSIONS = { - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.CAMERA - }; - ActivityCompat.requestPermissions(activity, PERMISSIONS, 1); - return false; + private boolean permissionsCheck(@NonNull final Activity activity, + @NonNull final Callback callback, + @NonNull final int requestCode) + { + final int writePermission = ActivityCompat + .checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); + final int cameraPermission = ActivityCompat + .checkSelfPermission(activity, Manifest.permission.CAMERA); + + final boolean permissionsGrated = writePermission == PackageManager.PERMISSION_GRANTED && + cameraPermission == PackageManager.PERMISSION_GRANTED; + + if (!permissionsGrated) + { + final Boolean dontAskAgain = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA); + + if (dontAskAgain) + { + final AlertDialog dialog = PermissionUtils + .explainingDialog(this, options, new PermissionUtils.OnExplainingPermissionCallback() + { + @Override + public void onCancel(WeakReference moduleInstance, + DialogInterface dialogInterface) + { + final ImagePickerModule module = moduleInstance.get(); + if (module == null) + { + return; + } + module.doOnCancel(); + } + + @Override + public void onReTry(WeakReference moduleInstance, + DialogInterface dialogInterface) + { + final ImagePickerModule module = moduleInstance.get(); + if (module == null) + { + return; + } + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", module.getContext().getPackageName(), null); + intent.setData(uri); + final Activity innerActivity = module.getActivity(); + if (innerActivity == null) + { + return; + } + innerActivity.startActivityForResult(intent, 1); + } + }); + dialog.show(); +// responseHelper.invokeError(callback, "Permissions weren't granted"); + return false; + } + else + { + String[] PERMISSIONS = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA}; + if (activity instanceof ReactActivity) + { + ((ReactActivity) activity).requestPermissions(PERMISSIONS, requestCode, listener); + } + else if (activity instanceof OnImagePickerPermissionsCallback) + { + ((OnImagePickerPermissionsCallback) activity).setPermissionListener(listener); + ActivityCompat.requestPermissions(activity, PERMISSIONS, requestCode); + } + else + { + final String errorDescription = new StringBuilder(activity.getClass().getSimpleName()) + .append(" must implement ") + .append(OnImagePickerPermissionsCallback.class.getSimpleName()) + .toString(); + throw new UnsupportedOperationException(errorDescription); + } + return false; + } } return true; } @@ -619,12 +738,14 @@ private File createNewFile() { return f; } - private void putExtraFileInfo(final String path, WritableMap response) { + private void putExtraFileInfo(@NonNull final String path, + @NonNull final ResponseHelper responseHelper) + { // size && filename try { File f = new File(path); - response.putDouble("fileSize", f.length()); - response.putString("fileName", f.getName()); + responseHelper.putDouble("fileSize", f.length()); + responseHelper.putString("fileName", f.getName()); } catch (Exception e) { e.printStackTrace(); } @@ -632,7 +753,7 @@ private void putExtraFileInfo(final String path, WritableMap response) { // type String extension = MimeTypeMap.getFileExtensionFromUrl(path); if (extension != null) { - response.putString("type", MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)); + responseHelper.putString("type", MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)); } } @@ -682,18 +803,27 @@ public void onScanCompleted(String path, Uri uri) { }); } - // Required for ActivityEventListener + @Override public void onNewIntent(Intent intent) { } - // Some people need this for compilation - public void onActivityResult(int requestCode, int resultCode, Intent data) { } + public Context getContext() + { + return getReactApplicationContext(); + } - private void cleanResponse() + public @StyleRes int getDialogThemeId() { - response = Arguments.createMap(); + return this.dialogThemeId; } - private static Uri compatUriFromFile(@NonNull final Context context, @NonNull final File file) { + public @NonNull Activity getActivity() + { + return getCurrentActivity(); + } + + private static @Nullable Uri compatUriFromFile(@NonNull final Context context, + @NonNull final File file) + { Uri result = null; if (Build.VERSION.SDK_INT < 19) { @@ -703,8 +833,15 @@ private static Uri compatUriFromFile(@NonNull final Context context, @NonNull fi { final String packageName = context.getApplicationContext().getPackageName(); final String authority = new StringBuilder(packageName).append(".provider").toString(); - result = FileProvider.getUriForFile(context, authority, file); + try + { + result = FileProvider.getUriForFile(context, authority, file); + } + catch(IllegalArgumentException e) + { + e.printStackTrace(); + } } return result; } -} +} \ No newline at end of file diff --git a/android/src/main/java/com/imagepicker/ImagePickerPackage.java b/android/src/main/java/com/imagepicker/ImagePickerPackage.java index ace2e0406..8fdb5b7b0 100644 --- a/android/src/main/java/com/imagepicker/ImagePickerPackage.java +++ b/android/src/main/java/com/imagepicker/ImagePickerPackage.java @@ -1,5 +1,7 @@ package com.imagepicker; +import android.support.annotation.StyleRes; + import com.facebook.react.ReactPackage; import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.NativeModule; @@ -11,9 +13,22 @@ import java.util.List; public class ImagePickerPackage implements ReactPackage { + public static final int DEFAULT_EXPLAINING_PERMISSION_DIALIOG_THEME = R.style.DefaultExplainingPermissionsTheme; + private @StyleRes final int dialogThemeId; + + public ImagePickerPackage() + { + this.dialogThemeId = DEFAULT_EXPLAINING_PERMISSION_DIALIOG_THEME; + } + + public ImagePickerPackage(@StyleRes final int dialogThemeId) + { + this.dialogThemeId = dialogThemeId; + } + @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new ImagePickerModule(reactContext)); + return Arrays.asList(new ImagePickerModule(reactContext, dialogThemeId)); } @Override diff --git a/android/src/main/java/com/imagepicker/ResponseHelper.java b/android/src/main/java/com/imagepicker/ResponseHelper.java new file mode 100644 index 000000000..4c7a9749a --- /dev/null +++ b/android/src/main/java/com/imagepicker/ResponseHelper.java @@ -0,0 +1,78 @@ +package com.imagepicker; + +import android.support.annotation.NonNull; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.WritableMap; + +/** + * Created by rusfearuth on 24.02.17. + */ + +public class ResponseHelper +{ + private WritableMap response = Arguments.createMap(); + + public void cleanResponse() + { + response = Arguments.createMap(); + } + + public @NonNull WritableMap getResponse() + { + return response; + } + + public void putString(@NonNull final String key, + @NonNull final String value) + { + response.putString(key, value); + } + + public void putInt(@NonNull final String key, + final int value) + { + response.putInt(key, value); + } + + public void putBoolean(@NonNull final String key, + final boolean value) + { + response.putBoolean(key, value); + } + + public void putDouble(@NonNull final String key, + final double value) + { + response.putDouble(key, value); + } + + public void invokeCustomButton(@NonNull final Callback callback, + @NonNull final String action) + { + cleanResponse(); + response.putString("customButton", action); + invokeResponse(callback); + } + + public void invokeCancel(@NonNull final Callback callback) + { + cleanResponse(); + response.putBoolean("didCancel", true); + invokeResponse(callback); + } + + public void invokeError(@NonNull final Callback callback, + @NonNull final String error) + { + cleanResponse(); + response.putString("error", error); + invokeResponse(callback); + } + + public void invokeResponse(@NonNull final Callback callback) + { + callback.invoke(response); + } +} diff --git a/android/src/main/java/com/imagepicker/permissions/OnImagePickerPermissionsCallback.java b/android/src/main/java/com/imagepicker/permissions/OnImagePickerPermissionsCallback.java new file mode 100644 index 000000000..4068288ad --- /dev/null +++ b/android/src/main/java/com/imagepicker/permissions/OnImagePickerPermissionsCallback.java @@ -0,0 +1,12 @@ +package com.imagepicker.permissions; + +import android.support.annotation.NonNull; +import com.facebook.react.modules.core.PermissionListener; + +/** + * Created by rusfearuth on 25.02.17. + */ +public interface OnImagePickerPermissionsCallback +{ + void setPermissionListener(@NonNull PermissionListener listener); +} diff --git a/android/src/main/java/com/imagepicker/permissions/PermissionUtils.java b/android/src/main/java/com/imagepicker/permissions/PermissionUtils.java new file mode 100644 index 000000000..8147114f5 --- /dev/null +++ b/android/src/main/java/com/imagepicker/permissions/PermissionUtils.java @@ -0,0 +1,74 @@ +package com.imagepicker.permissions; + +import android.app.Activity; +import android.content.DialogInterface; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.app.AlertDialog; + +import com.facebook.react.bridge.ReadableMap; +import com.imagepicker.ImagePickerModule; +import com.imagepicker.R; + +import java.lang.ref.WeakReference; + +/** + * Created by rusfearuth on 03.03.17. + */ + +public class PermissionUtils +{ + public static @Nullable AlertDialog explainingDialog(@NonNull final ImagePickerModule module, + @NonNull final ReadableMap options, + @NonNull final OnExplainingPermissionCallback callback) + { + if (module.getContext() == null) + { + return null; + } + final ReadableMap permissionDenied = options.getMap("permissionDenied"); + final String title = permissionDenied.getString("title"); + final String text = permissionDenied.getString("text"); + final String btnReTryTitle = permissionDenied.getString("reTryTitle"); + final String btnOkTitle = permissionDenied.getString("okTitle"); + final WeakReference reference = new WeakReference<>(module); + + final Activity activity = module.getActivity(); + + if (activity == null) + { + return null; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(activity, module.getDialogThemeId()); + builder + .setTitle(title) + .setMessage(text) + .setCancelable(false) + .setNegativeButton(btnOkTitle, new DialogInterface.OnClickListener() + { + @Override + public void onClick(final DialogInterface dialogInterface, + int i) + { + callback.onCancel(reference, dialogInterface); + } + }) + .setPositiveButton(btnReTryTitle, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialogInterface, + int i) + { + callback.onReTry(reference, dialogInterface); + } + }); + + return builder.create(); + } + + public interface OnExplainingPermissionCallback { + void onCancel(WeakReference moduleInstance, DialogInterface dialogInterface); + void onReTry(WeakReference moduleInstance, DialogInterface dialogInterface); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/imagepicker/permissions/PermissionsHelper.java b/android/src/main/java/com/imagepicker/permissions/PermissionsHelper.java new file mode 100644 index 000000000..2d8983441 --- /dev/null +++ b/android/src/main/java/com/imagepicker/permissions/PermissionsHelper.java @@ -0,0 +1,11 @@ +package com.imagepicker.permissions; + +import android.support.annotation.NonNull; + +/** + * Created by rusfearuth on 03.03.17. + */ + +public class PermissionsHelper +{ +} diff --git a/android/src/main/java/com/imagepicker/utils/ButtonsHelper.java b/android/src/main/java/com/imagepicker/utils/ButtonsHelper.java index 7fbc4c02d..dbb7115dd 100644 --- a/android/src/main/java/com/imagepicker/utils/ButtonsHelper.java +++ b/android/src/main/java/com/imagepicker/utils/ButtonsHelper.java @@ -64,11 +64,6 @@ public List getTitles() result.add(customButtons.get(i).title); } - if (btnCancel != null) - { - result.add(btnCancel.title); - } - return result; } @@ -91,11 +86,6 @@ public List getActions() result.add(customButtons.get(i).action); } - if (btnCancel != null) - { - result.add(btnCancel.action); - } - return result; } diff --git a/android/src/main/java/com/imagepicker/utils/UI.java b/android/src/main/java/com/imagepicker/utils/UI.java index d4c06d2b4..3e6ee8de8 100644 --- a/android/src/main/java/com/imagepicker/utils/UI.java +++ b/android/src/main/java/com/imagepicker/utils/UI.java @@ -1,14 +1,17 @@ package com.imagepicker.utils; -import android.app.AlertDialog; +//import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.graphics.drawable.ColorDrawable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.app.AlertDialog; import android.widget.ArrayAdapter; import com.facebook.react.bridge.ReadableMap; +import com.imagepicker.ImagePickerModule; +import com.imagepicker.R; import java.util.List; @@ -17,19 +20,25 @@ */ public class UI { - public static void showDialog(@Nullable final Context context, - @NonNull final ReadableMap options, - @Nullable final OnAction callback) + public static @NonNull AlertDialog chooseDialog(@Nullable final ImagePickerModule module, + @NonNull final ReadableMap options, + @Nullable final OnAction callback) { + final Context context = module.getActivity(); + if (context == null) + { + return null; + } + final ButtonsHelper buttons = ButtonsHelper.newInstance(options); final List titles = buttons.getTitles(); final List actions = buttons.getActions(); ArrayAdapter adapter = new ArrayAdapter<>( context, - android.R.layout.select_dialog_item, + R.layout.list_item, titles ); - AlertDialog.Builder builder = new AlertDialog.Builder(context, android.R.style.Theme_Holo_Light_Dialog); + AlertDialog.Builder builder = new AlertDialog.Builder(context, module.getDialogThemeId() /*android.R.style.Theme_Holo_Light_Dialog*/); if (ReadableMapUtils.hasAndNotEmpty(options, "title")) { builder.setTitle(options.getString("title")); @@ -58,6 +67,17 @@ public void onClick(DialogInterface dialog, int index) { } }); + builder.setNegativeButton(buttons.btnCancel.title, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialogInterface, + int i) + { + callback.onCancel(); + dialogInterface.dismiss(); + } + }); + final AlertDialog dialog = builder.create(); dialog.setOnCancelListener(new DialogInterface.OnCancelListener() @@ -65,13 +85,13 @@ public void onClick(DialogInterface dialog, int index) { @Override public void onCancel(@NonNull final DialogInterface dialog) { - callback.onDialogWasCanceled("didCancel"); + callback.onCancel(); dialog.dismiss(); } }); - dialog.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT)); - dialog.show(); + //dialog.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT)); + return dialog; } public interface OnAction @@ -80,6 +100,5 @@ public interface OnAction void onUseLibrary(); void onCancel(); void onCustomButton(String action); - void onDialogWasCanceled(String action); } } diff --git a/android/src/main/res/layout/list_item.xml b/android/src/main/res/layout/list_item.xml new file mode 100644 index 000000000..2530f04fd --- /dev/null +++ b/android/src/main/res/layout/list_item.xml @@ -0,0 +1,15 @@ + + \ No newline at end of file diff --git a/android/src/main/res/values/themes.xml b/android/src/main/res/values/themes.xml new file mode 100644 index 000000000..6befbd04d --- /dev/null +++ b/android/src/main/res/values/themes.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/index.js b/index.js index 2e0a35606..6a810a019 100644 --- a/index.js +++ b/index.js @@ -2,13 +2,20 @@ const { NativeModules } = require('react-native'); const { ImagePickerManager } = NativeModules; + const DEFAULT_OPTIONS = { title: 'Select a Photo', cancelButtonTitle: 'Cancel', takePhotoButtonTitle: 'Take Photo…', chooseFromLibraryButtonTitle: 'Choose from Library…', quality: 1.0, - allowsEditing: false + allowsEditing: false, + permissionDenied: { + title: 'Permission denied', + text: 'To be able to take pictures with your camera and choose images from your library.', + reTryTitle: 're-try', + okTitle: 'I\'m sure', + } }; module.exports = { diff --git a/package.json b/package.json index 255e74b9e..e46c60617 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,10 @@ "picker" ], "scripts": { - "prepublish": "rm -rf android/build Example/android/build Example/android/app/build" + "prepublish": "rm -rf android/build Example/android/build Example/android/app/build node_modules" }, - "types": "./index.d.ts" + "types": "./index.d.ts", + "peerDependencies": { + "react-native": "^0.42.0" + } }