diff --git a/Examples/UIExplorer/android/app/build.gradle b/Examples/UIExplorer/android/app/build.gradle index 6a24213d1..79cbe0b95 100644 --- a/Examples/UIExplorer/android/app/build.gradle +++ b/Examples/UIExplorer/android/app/build.gradle @@ -89,7 +89,7 @@ android { defaultConfig { applicationId "com.facebook.react.uiapp" minSdkVersion 16 - targetSdkVersion 22 + targetSdkVersion 23 versionCode 1 versionName "1.0" ndk { diff --git a/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml index bd77d77e4..308604da5 100644 --- a/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml +++ b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml @@ -11,6 +11,10 @@ + + + + diff --git a/Examples/UIExplorer/js/PermissionsExampleAndroid.android.js b/Examples/UIExplorer/js/PermissionsExampleAndroid.android.js index 129955ba4..8a5bc5288 100644 --- a/Examples/UIExplorer/js/PermissionsExampleAndroid.android.js +++ b/Examples/UIExplorer/js/PermissionsExampleAndroid.android.js @@ -27,13 +27,15 @@ const React = require('react'); const ReactNative = require('react-native'); const { PermissionsAndroid, + Picker, StyleSheet, Text, - TextInput, TouchableWithoutFeedback, View, } = ReactNative; +const Item = Picker.Item; + exports.displayName = (undefined: ?string); exports.framework = 'React'; exports.title = 'PermissionsAndroid'; @@ -41,7 +43,7 @@ exports.description = 'Permissions example for API 23+.'; class PermissionsExample extends React.Component { state = { - permission: PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, + permission: PermissionsAndroid.PERMISSIONS.CAMERA, hasPermission: 'Not Checked', }; @@ -49,13 +51,14 @@ class PermissionsExample extends React.Component { return ( Permission Name: - + + + + + Check Permission @@ -71,14 +74,14 @@ class PermissionsExample extends React.Component { ); } - _updateText = (event: Object) => { + _onSelectPermission = (permission: string) => { this.setState({ - permission: event.nativeEvent.text, + permission: permission, }); }; _checkPermission = async () => { - let result = await PermissionsAndroid.checkPermission(this.state.permission); + let result = await PermissionsAndroid.check(this.state.permission); this.setState({ hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' + this.state.permission, @@ -86,7 +89,7 @@ class PermissionsExample extends React.Component { }; _requestPermission = async () => { - let result = await PermissionsAndroid.requestPermission( + let result = await PermissionsAndroid.request( this.state.permission, { title: 'Permission Explanation', @@ -95,8 +98,9 @@ class PermissionsExample extends React.Component { ' because of reasons. Please approve.' }, ); + this.setState({ - hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' + + hasPermission: result + ' for ' + this.state.permission, }); }; @@ -125,4 +129,7 @@ var styles = StyleSheet.create({ touchable: { color: '#007AFF', }, + picker: { + flex: 1, + } }); diff --git a/Libraries/PermissionsAndroid/PermissionsAndroid.js b/Libraries/PermissionsAndroid/PermissionsAndroid.js index aaa1ea9cf..524bfffb4 100644 --- a/Libraries/PermissionsAndroid/PermissionsAndroid.js +++ b/Libraries/PermissionsAndroid/PermissionsAndroid.js @@ -19,6 +19,8 @@ type Rationale = { message: string; } +type PermissionStatus = 'granted' | 'denied' | 'never_ask_again'; + /** * `PermissionsAndroid` provides access to Android M's new permissions model. * Some permissions are granted by default when the application is installed @@ -47,7 +49,7 @@ type Rationale = { * 'so you can take awesome pictures.' * } * ) - * if (granted) { + * if (granted === PermissionsAndroid.RESULTS.GRANTED) { * console.log("You can use the camera") * } else { * console.log("Camera permission denied") @@ -61,6 +63,7 @@ type Rationale = { class PermissionsAndroid { PERMISSIONS: Object; + RESULTS: Object; constructor() { /** @@ -92,17 +95,38 @@ class PermissionsAndroid { READ_EXTERNAL_STORAGE: 'android.permission.READ_EXTERNAL_STORAGE', WRITE_EXTERNAL_STORAGE: 'android.permission.WRITE_EXTERNAL_STORAGE', }; + + this.RESULTS = { + GRANTED: 'granted', + DENIED: 'denied', + NEVER_ASK_AGAIN: 'never_ask_again', + }; + } + + /** + * DEPRECATED - use check + * + * Returns a promise resolving to a boolean value as to whether the specified + * permissions has been granted + * + * @deprecated + */ + checkPermission(permission: string) : Promise { + console.warn('"PermissionsAndroid.checkPermission" is deprecated. Use "PermissionsAndroid.check" instead'); + return Permissions.checkPermission(permission); } /** * Returns a promise resolving to a boolean value as to whether the specified * permissions has been granted */ - checkPermission(permission: string) : Promise { + check(permission: string) : Promise { return Permissions.checkPermission(permission); } /** + * DEPRECATED - use request + * * Prompts the user to enable a permission and returns a promise resolving to a * boolean value indicating whether the user allowed or denied the request * @@ -111,8 +135,26 @@ class PermissionsAndroid { * necessary to show a dialog explaining why the permission is needed * (https://developer.android.com/training/permissions/requesting.html#explain) * and then shows the system permission dialog + * + * @deprecated */ async requestPermission(permission: string, rationale?: Rationale) : Promise { + console.warn('"PermissionsAndroid.requestPermission" is deprecated. Use "PermissionsAndroid.request" instead'); + const response = await this.request(permission, rationale); + return (response === this.RESULTS.GRANTED); + } + + /** + * Prompts the user to enable a permission and returns a promise resolving to a + * string value indicating whether the user allowed or denied the request + * + * If the optional rationale argument is included (which is an object with a + * `title` and `message`), this function checks with the OS whether it is + * necessary to show a dialog explaining why the permission is needed + * (https://developer.android.com/training/permissions/requesting.html#explain) + * and then shows the system permission dialog + */ + async request(permission: string, rationale?: Rationale) : Promise { if (rationale) { const shouldShowRationale = await Permissions.shouldShowRequestPermissionRationale(permission); @@ -128,6 +170,15 @@ class PermissionsAndroid { } return Permissions.requestPermission(permission); } + + /** + * Prompts the user to enable multiple permissions in the same dialog and + * returns an object with the permissions as keys and strings as values + * indicating whether the user allowed or denied the request + */ + requestMultiple(permissions: Array) : Promise<{[permission: string]: PermissionStatus}> { + return Permissions.requestMultiplePermissions(permissions); + } } PermissionsAndroid = new PermissionsAndroid(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index 5afb72203..2791564f8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -2,8 +2,6 @@ package com.facebook.react; -import javax.annotation.Nullable; - import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; @@ -17,11 +15,14 @@ import android.widget.Toast; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.Callback; import com.facebook.react.common.ReactConstants; import com.facebook.react.devsupport.DoubleTapReloadRecognizer; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.PermissionListener; +import javax.annotation.Nullable; + /** * Delegate class for {@link ReactActivity} and {@link ReactFragmentActivity}. You can subclass this * to provide custom implementations for e.g. {@link #getReactNativeHost()}, if your Application @@ -38,6 +39,7 @@ public class ReactActivityDelegate { private @Nullable ReactRootView mReactRootView; private @Nullable DoubleTapReloadRecognizer mDoubleTapReloadRecognizer; private @Nullable PermissionListener mPermissionListener; + private @Nullable Callback mPermissionsCallback; public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) { mActivity = activity; @@ -117,6 +119,11 @@ public class ReactActivityDelegate { getPlainActivity(), (DefaultHardwareBackBtnHandler) getPlainActivity()); } + + if (mPermissionsCallback != null) { + mPermissionsCallback.invoke(); + mPermissionsCallback = null; + } } protected void onDestroy() { @@ -178,13 +185,18 @@ public class ReactActivityDelegate { } public void onRequestPermissionsResult( - int requestCode, - String[] permissions, - int[] grantResults) { - if (mPermissionListener != null && - mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) { - mPermissionListener = null; - } + final int requestCode, + final String[] permissions, + final int[] grantResults) { + mPermissionsCallback = new Callback() { + @Override + public void invoke(Object... args) { + if (mPermissionListener != null && + mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) { + mPermissionListener = null; + } + } + }; } private Context getContext() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/permissions/PermissionsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/permissions/PermissionsModule.java index f00a9d788..605496434 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/permissions/PermissionsModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/permissions/PermissionsModule.java @@ -21,10 +21,15 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.core.PermissionAwareActivity; import com.facebook.react.modules.core.PermissionListener; +import java.util.ArrayList; + /** * Module that exposes the Android M Permission system to JS. */ @@ -34,6 +39,9 @@ public class PermissionsModule extends ReactContextBaseJavaModule implements Per private static final String ERROR_INVALID_ACTIVITY = "E_INVALID_ACTIVITY"; private final SparseArray mCallbacks; private int mRequestCode = 0; + private final String GRANTED = "granted"; + private final String DENIED = "denied"; + private final String NEVER_ASK_AGAIN = "never_ask_again"; public PermissionsModule(ReactApplicationContext reactContext) { super(reactContext); @@ -96,7 +104,7 @@ public class PermissionsModule extends ReactContextBaseJavaModule implements Per return; } if (context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { - promise.resolve(true); + promise.resolve(GRANTED); return; } @@ -107,9 +115,20 @@ public class PermissionsModule extends ReactContextBaseJavaModule implements Per mRequestCode, new Callback() { @Override public void invoke(Object... args) { - promise.resolve(args[0].equals(PackageManager.PERMISSION_GRANTED)); + int[] results = (int[]) args[0]; + if (results[0] == PackageManager.PERMISSION_GRANTED) { + promise.resolve(GRANTED); + } else { + PermissionAwareActivity activity = (PermissionAwareActivity) args[1]; + if (activity.shouldShowRequestPermissionRationale(permission)) { + promise.resolve(DENIED); + } else { + promise.resolve(NEVER_ASK_AGAIN); + } + } } - }); + } + ); activity.requestPermissions(new String[]{permission}, mRequestCode, this); mRequestCode++; @@ -118,17 +137,76 @@ public class PermissionsModule extends ReactContextBaseJavaModule implements Per } } + @ReactMethod + public void requestMultiplePermissions(final ReadableArray permissions, final Promise promise) { + final WritableMap grantedPermissions = new WritableNativeMap(); + final ArrayList permissionsToCheck = new ArrayList(); + int checkedPermissionsCount = 0; + + Context context = getReactApplicationContext().getBaseContext(); + + for (int i = 0; i < permissions.size(); i++) { + String perm = permissions.getString(i); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + grantedPermissions.putString(perm, context.checkPermission(perm, Process.myPid(), Process.myUid()) == + PackageManager.PERMISSION_GRANTED ? GRANTED : DENIED); + checkedPermissionsCount++; + } else if (context.checkSelfPermission(perm) == PackageManager.PERMISSION_GRANTED) { + grantedPermissions.putString(perm, GRANTED); + checkedPermissionsCount++; + } else { + permissionsToCheck.add(perm); + } + } + if (permissions.size() == checkedPermissionsCount) { + promise.resolve(grantedPermissions); + return; + } + try { + + PermissionAwareActivity activity = getPermissionAwareActivity(); + + mCallbacks.put( + mRequestCode, new Callback() { + @Override + public void invoke(Object... args) { + int[] results = (int[]) args[0]; + PermissionAwareActivity activity = (PermissionAwareActivity) args[1]; + for (int j = 0; j < permissionsToCheck.size(); j++) { + String permission = permissionsToCheck.get(j); + if (results[j] == PackageManager.PERMISSION_GRANTED) { + grantedPermissions.putString(permission, GRANTED); + } else { + if (activity.shouldShowRequestPermissionRationale(permission)) { + grantedPermissions.putString(permission, DENIED); + } else { + grantedPermissions.putString(permission, NEVER_ASK_AGAIN); + } + } + } + promise.resolve(grantedPermissions); + } + }); + + activity.requestPermissions(permissionsToCheck.toArray(new String[0]), mRequestCode, this); + mRequestCode++; + } catch (IllegalStateException e) { + promise.reject(ERROR_INVALID_ACTIVITY, e); + } + } + /** * Method called by the activity with the result of the permission request. */ @Override public boolean onRequestPermissionsResult( - int requestCode, - String[] permissions, - int[] grantResults) { - mCallbacks.get(requestCode).invoke(grantResults[0]); - mCallbacks.remove(requestCode); - return mCallbacks.size() == 0; + int requestCode, + String[] permissions, + int[] grantResults) { + mCallbacks.get(requestCode).invoke(grantResults, getPermissionAwareActivity()); + mCallbacks.remove(requestCode); + return mCallbacks.size() == 0; } private PermissionAwareActivity getPermissionAwareActivity() {