From 59ce8571c349b6c3feb96697b56bd38fbee7b327 Mon Sep 17 00:00:00 2001 From: enahum Date: Thu, 26 Oct 2017 09:17:56 -0300 Subject: [PATCH] Handle Image picker permissions (#1064) --- ImagePickerModule.java | 37 +++++++++--- .../components/attachment_button.js | 60 +++++++++++++++++-- assets/base/i18n/en.json | 8 +++ 3 files changed, 93 insertions(+), 12 deletions(-) diff --git a/ImagePickerModule.java b/ImagePickerModule.java index 8367c59e5..b3b3ee9c6 100644 --- a/ImagePickerModule.java +++ b/ImagePickerModule.java @@ -6,8 +6,10 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.ResolveInfo; import android.graphics.BitmapFactory; import android.net.Uri; +import android.os.Build; import android.provider.MediaStore; import android.provider.Settings; import android.support.annotation.NonNull; @@ -44,6 +46,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.ref.WeakReference; +import java.util.List; import com.facebook.react.modules.core.PermissionListener; import com.facebook.react.modules.core.PermissionAwareActivity; @@ -196,7 +199,9 @@ public class ImagePickerModule extends ReactContextBaseJavaModule public void doOnCancel() { - responseHelper.invokeCancel(callback); + if (this.callback != null) { + responseHelper.invokeCancel(this.callback); + } } public void launchCamera() @@ -221,6 +226,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule return; } + this.callback = callback; this.options = options; if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_CAMERA)) @@ -251,7 +257,12 @@ public class ImagePickerModule extends ReactContextBaseJavaModule final File original = createNewFile(reactContext, this.options, false); imageConfig = imageConfig.withOriginalFile(original); - cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original); + if (imageConfig.original != null) { + cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original); + }else { + responseHelper.invokeError(callback, "Couldn't get file path for photo"); + return; + } if (cameraCaptureURI == null) { responseHelper.invokeError(callback, "Couldn't get file path for photo"); @@ -266,7 +277,16 @@ public class ImagePickerModule extends ReactContextBaseJavaModule return; } - this.callback = callback; + // Workaround for Android bug. + // grantUriPermission also needed for KITKAT, + // see https://code.google.com/p/android/issues/detail?id=76683 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + List resInfoList = reactContext.getPackageManager().queryIntentActivities(cameraIntent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + reactContext.grantUriPermission(packageName, cameraCaptureURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } try { @@ -294,6 +314,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule } this.options = options; + this.callback = callback; if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_LIBRARY)) { @@ -314,7 +335,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule { requestCode = REQUEST_LAUNCH_IMAGE_LIBRARY; libraryIntent = new Intent(Intent.ACTION_PICK, - MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + MediaStore.Images.Media.EXTERNAL_CONTENT_URI); } if (libraryIntent.resolveActivity(reactContext.getPackageManager()) == null) @@ -323,8 +344,6 @@ public class ImagePickerModule extends ReactContextBaseJavaModule return; } - this.callback = callback; - try { currentActivity.startActivityForResult(libraryIntent, requestCode); @@ -571,7 +590,9 @@ public class ImagePickerModule extends ReactContextBaseJavaModule innerActivity.startActivityForResult(intent, 1); } }); - dialog.show(); + if (dialog != null) { + dialog.show(); + } return false; } else @@ -606,7 +627,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule private boolean isCameraAvailable() { return reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) - || reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + || reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); } private @NonNull String getRealPathFromURI(@NonNull final Uri uri) { diff --git a/app/components/post_textbox/components/attachment_button.js b/app/components/post_textbox/components/attachment_button.js index 8e219bd72..107e709a2 100644 --- a/app/components/post_textbox/components/attachment_button.js +++ b/app/components/post_textbox/components/attachment_button.js @@ -1,5 +1,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; +import {injectIntl, intlShape} from 'react-intl'; import { Platform, StyleSheet, @@ -10,21 +11,38 @@ import ImagePicker from 'react-native-image-picker'; import {changeOpacity} from 'app/utils/theme'; -export default class AttachmentButton extends PureComponent { +class AttachmentButton extends PureComponent { static propTypes = { blurTextBox: PropTypes.func.isRequired, + intl: intlShape.isRequired, navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, uploadFiles: PropTypes.func.isRequired }; attachFileFromCamera = () => { + const {formatMessage} = this.props.intl; const options = { quality: 0.7, noData: true, storageOptions: { cameraRoll: true, waitUntilSaved: true + }, + permissionDenied: { + title: formatMessage({ + id: 'mobile.android.camera_permission_denied_title', + defaultMessage: 'Camera access is required' + }), + text: formatMessage({ + id: 'mobile.android.camera_permission_denied_description', + defaultMessage: 'To take photos and videos with your camera, please change your permission settings.' + }), + reTryTitle: formatMessage({ + id: 'mobile.android.permission_denied_retry', + defaultMessage: 'Set Permission' + }), + okTitle: formatMessage({id: 'mobile.android.permission_denied_dismiss', defaultMessage: 'Dismiss'}) } }; @@ -38,9 +56,25 @@ export default class AttachmentButton extends PureComponent { }; attachFileFromLibrary = () => { + const {formatMessage} = this.props.intl; const options = { quality: 0.7, - noData: true + noData: true, + permissionDenied: { + title: formatMessage({ + id: 'mobile.android.photos_permission_denied_title', + defaultMessage: 'Photo library access is required' + }), + text: formatMessage({ + id: 'mobile.android.photos_permission_denied_description', + defaultMessage: 'To upload images from your library, please change your permission settings.' + }), + reTryTitle: formatMessage({ + id: 'mobile.android.permission_denied_retry', + defaultMessage: 'Set Permission' + }), + okTitle: formatMessage({id: 'mobile.android.permission_denied_dismiss', defaultMessage: 'Dismiss'}) + } }; if (Platform.OS === 'ios') { @@ -57,10 +91,26 @@ export default class AttachmentButton extends PureComponent { }; attachVideoFromLibraryAndroid = () => { + const {formatMessage} = this.props.intl; const options = { quality: 0.7, mediaType: 'video', - noData: true + noData: true, + permissionDenied: { + title: formatMessage({ + id: 'mobile.android.videos_permission_denied_title', + defaultMessage: 'Video library access is required' + }), + text: formatMessage({ + id: 'mobile.android.videos_permission_denied_description', + defaultMessage: 'To upload videos from your library, please change your permission settings.' + }), + reTryTitle: formatMessage({ + id: 'mobile.android.permission_denied_retry', + defaultMessage: 'Set Permission' + }), + okTitle: formatMessage({id: 'mobile.android.permission_denied_dismiss', defaultMessage: 'Dismiss'}) + } }; ImagePicker.launchImageLibrary(options, (response) => { @@ -70,7 +120,7 @@ export default class AttachmentButton extends PureComponent { this.uploadFiles([response]); }); - } + }; uploadFiles = (images) => { this.props.uploadFiles(images); @@ -175,3 +225,5 @@ const style = StyleSheet.create({ justifyContent: 'center' } }); + +export default injectIntl(AttachmentButton); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index d054187c1..879d8cea8 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -1880,6 +1880,14 @@ "mobile.advanced_settings.reset_message": "\nThis will reset all offline data and restart the app. You will be automatically logged back in once the app restarts.\n", "mobile.advanced_settings.reset_title": "Reset Cache", "mobile.advanced_settings.title": "Advanced Settings", + "mobile.android.camera_permission_denied_title": "Camera access is required", + "mobile.android.camera_permission_denied_description": "To take photos and videos with your camera, please change your permission settings.", + "mobile.android.photos_permission_denied_title": "Photo library access is required", + "mobile.android.photos_permission_denied_description": "To upload images from your library, please change your permission settings.", + "mobile.android.videos_permission_denied_title": "Video library access is required", + "mobile.android.videos_permission_denied_description": "To upload videos from your library, please change your permission settings.", + "mobile.android.permission_denied_retry": "Set permission", + "mobile.android.permission_denied_dismiss": "Dismiss", "mobile.channel_drawer.search": "Jump to...", "mobile.channel_info.alertMessageDeleteChannel": "Are you sure you want to delete the {term} {name}?", "mobile.channel_info.alertMessageLeaveChannel": "Are you sure you want to leave the {term} {name}?",