From b7352b46671d09471d6aa9355ea41368ea05df96 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Wed, 15 Jun 2016 09:42:51 -0700 Subject: [PATCH] Open source permissions Summary: This moves into open source the PermissionsModule and the activity and listener interfaces necessary to make permissions work. It also moves the PermissionsExample into the UIExplorer. In order to make this work, the device has to be Android M and above, and the target sdk of the app has to be 23+, so I changed the uiexplorer manifest to target that API. This has the unfortunate consequence that people testing on devices with API 23+ will have to enable the `draw over other apps` setting that react needs for RedBoxing. The app will automatically send the user to that screen, so enabling the setting and resuming the app should be trivial. For testing, try requesting permission for a permission that is currently revoked. If a permission is granted, it can be revoked via adb (`adb shell pm revoke com.your.app android.permission.PERMISSION_NAME`), and then requested. Reviewed By: bestander Differential Revision: D3431324 fbshipit-source-id: 8cbaea676d2b5727cb5191cdb77a02e213bf9ba3 --- .../PermissionsExampleAndroid.android.js | 155 ++++++++++++++++++ Examples/UIExplorer/UIExplorerList.android.js | 4 + .../android/app/src/main/AndroidManifest.xml | 2 +- .../com/facebook/react/ReactActivity.java | 36 +++- .../modules/core/PermissionAwareActivity.java | 39 +++++ .../modules/core/PermissionListener.java | 27 +++ .../facebook/react/modules/permissions/BUCK | 20 +++ .../permissions/PermissionsModule.java | 147 +++++++++++++++++ .../main/java/com/facebook/react/shell/BUCK | 1 + .../react/shell/MainReactPackage.java | 2 + 10 files changed, 425 insertions(+), 8 deletions(-) create mode 100644 Examples/UIExplorer/PermissionsExampleAndroid.android.js create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/PermissionAwareActivity.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/PermissionListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/permissions/BUCK create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/permissions/PermissionsModule.java diff --git a/Examples/UIExplorer/PermissionsExampleAndroid.android.js b/Examples/UIExplorer/PermissionsExampleAndroid.android.js new file mode 100644 index 000000000..bde582110 --- /dev/null +++ b/Examples/UIExplorer/PermissionsExampleAndroid.android.js @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule PermissionsExampleAndroid + * @flow + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + StyleSheet, + Text, + TextInput, + TouchableWithoutFeedback, + View, +} = ReactNative; +const DialogManager = require('NativeModules').DialogManagerAndroid; +const Permissions = require('NativeModules').AndroidPermissions; + +exports.displayName = (undefined: ?string); +exports.framework = 'React'; +exports.title = ''; +exports.description = 'Permissions example for API 23+.'; + +const PermissionsExample = React.createClass({ + getInitialState: function() { + return { + permission: 'android.permission.WRITE_EXTERNAL_STORAGE', + hasPermission: 'Not Checked', + }; + }, + + render: function() { + return ( + + Permission Name: + + + + Check Permission + + + Permission Status: {this.state.hasPermission} + + + Request Permission + + + + ); + }, + + _updateText: function(event: Object) { + this.setState({ + permission: event.nativeEvent.text, + }); + }, + + _checkPermission: function() { + Permissions.checkPermission( + this.state.permission, + (permission: string, result: boolean) => { + this.setState({ + hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' + permission, + }); + }, + this._showError); + }, + + _shouldExplainPermission: function() { + Permissions.shouldShowRequestPermissionRationale( + this.state.permission, + (permission: string, shouldShow: boolean) => { + if (shouldShow) { + DialogManager.showAlert( + { + title: 'Permission Explanation', + message: + 'The app needs the following permission ' + this.state.permission + + ' because of reasons. Please approve.' + }, + this._showError, + this._requestPermission); + } else { + this._requestPermission(); + } + }, + this._showError); + }, + + _requestPermission: function() { + Permissions.requestPermission( + this.state.permission, + (permission: string, result: boolean) => { + this.setState({ + hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' + permission, + }); + }, + this._showError); + }, + + _showError: function() { + DialogManager.showAlert({message: 'Error'}, {}, {}); + } +}); + +exports.examples = [ + { + title: 'Permissions Example', + description: 'Short example of how to use the runtime permissions API introduced in Android M.', + render: () => , + }, +]; + +var styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + }, + singleLine: { + fontSize: 16, + padding: 4, + }, + text: { + margin: 10, + }, + touchable: { + color: '#007AFF', + }, +}); + diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index c4c7aecc9..4fdfd4afb 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -169,6 +169,10 @@ const APIExamples = [ key: 'PanResponderExample', module: require('./PanResponderExample'), }, + { + key: 'PermissionsExampleAndroid', + module: require('./PermissionsExampleAndroid'), + }, { key: 'PointerEventsExample', module: require('./PointerEventsExample'), diff --git a/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml index be53ed1b4..8efcb2ef4 100644 --- a/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml +++ b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ + android:targetSdkVersion="23" /> mCallbacks; + private int mRequestCode = 0; + + public PermissionsModule(ReactApplicationContext reactContext) { + super(reactContext); + mCallbacks = new SparseArray(); + } + + @Override + public String getName() { + return "AndroidPermissions"; + } + + /** + * Check if the app has the permission given. successCallback is called with true if the + * permission had been granted, false otherwise. See {@link Activity#checkSelfPermission}. + */ + @ReactMethod + public void checkPermission( + final String permission, + final Callback successCallback, + final Callback errorCallback) { + PermissionAwareActivity activity = getPermissionAwareActivity(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + successCallback.invoke( + permission, + activity.checkPermission(permission, Process.myPid(), Process.myUid()) == + PackageManager.PERMISSION_GRANTED); + return; + } + successCallback.invoke( + permission, + activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED); + } + + /** + * Check whether the app should display a message explaining why a certain permission is needed. + * successCallback is called with true if the app should display a message, false otherwise. + * This message is only displayed if the user has revoked this permission once before, and if the + * permission dialog will be shown to the user (the user can choose to not be shown that dialog + * again). For devices before Android M, this always returns false. + * See {@link Activity#shouldShowRequestPermissionRationale}. + */ + @ReactMethod + public void shouldShowRequestPermissionRationale( + final String permission, + final Callback successCallback, + final Callback errorCallback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + successCallback.invoke(permission, false); + return; + } + successCallback.invoke( + permission, + getPermissionAwareActivity().shouldShowRequestPermissionRationale(permission)); + } + + /** + * Request the given permission. successCallback is called with true if the permission had been + * granted, false otherwise. For devices before Android M, this instead checks if the user has + * the permission given or not. + * See {@link Activity#checkSelfPermission}. + */ + @ReactMethod + public void requestPermission( + final String permission, + final Callback successCallback, + final Callback errorCallback) { + PermissionAwareActivity activity = getPermissionAwareActivity(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + successCallback.invoke( + permission, + activity.checkPermission(permission, Process.myPid(), Process.myUid()) == + PackageManager.PERMISSION_GRANTED); + return; + } + if (activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { + successCallback.invoke(permission, true); + return; + } + + mCallbacks.put( + mRequestCode, new Callback() { + @Override + public void invoke(Object... args) { + successCallback.invoke(permission, args[0].equals(PackageManager.PERMISSION_GRANTED)); + } + }); + + activity.requestPermissions(new String[]{permission}, mRequestCode, this); + mRequestCode++; + } + + /** + * 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; + } + + private PermissionAwareActivity getPermissionAwareActivity() { + Activity activity = getCurrentActivity(); + if (activity == null) { + throw new IllegalStateException("Tried to use permissions API while not attached to an " + + "Activity."); + } else if (!(activity instanceof PermissionAwareActivity)) { + throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't" + + " implement PermissionAwareActivity."); + } + return (PermissionAwareActivity) activity; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index 432279a92..9882cbdbd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -24,6 +24,7 @@ android_library( react_native_target('java/com/facebook/react/modules/location:location'), react_native_target('java/com/facebook/react/modules/netinfo:netinfo'), react_native_target('java/com/facebook/react/modules/network:network'), + react_native_target('java/com/facebook/react/modules/permissions:permissions'), react_native_target('java/com/facebook/react/modules/statusbar:statusbar'), react_native_target('java/com/facebook/react/modules/storage:storage'), react_native_target('java/com/facebook/react/modules/timepicker:timepicker'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index c17cfe8d0..a07429e7e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -30,6 +30,7 @@ import com.facebook.react.modules.intent.IntentModule; import com.facebook.react.modules.location.LocationModule; import com.facebook.react.modules.netinfo.NetInfoModule; import com.facebook.react.modules.network.NetworkingModule; +import com.facebook.react.modules.permissions.PermissionsModule; import com.facebook.react.modules.statusbar.StatusBarModule; import com.facebook.react.modules.storage.AsyncStorageModule; import com.facebook.react.modules.timepicker.TimePickerDialogModule; @@ -83,6 +84,7 @@ public class MainReactPackage implements ReactPackage { new LocationModule(reactContext), new NetworkingModule(reactContext), new NetInfoModule(reactContext), + new PermissionsModule(reactContext), new StatusBarModule(reactContext), new TimePickerDialogModule(reactContext), new ToastModule(reactContext),