From 0fb2ccfcc3e6babe28d7d1bcf4635ef2f9191eb1 Mon Sep 17 00:00:00 2001 From: Connor McEwen Date: Mon, 15 Aug 2016 05:52:04 -0700 Subject: [PATCH] Add JS library for requesting Android M Permissions Summary: Explain the **motivation** for making this change. What existing problem does the pull request solve? The Android permissions native module was open sourced recently (https://github.com/facebook/react-native/commit/b7352b46671d09471d6aa9355ea41368ea05df96) but it is currently undocumented and requires directly interfacing with the native module. This provides a JS wrapper to make it easier to use the permissions module and documents it. This could be cleaner if the native code used Promise blocks instead of callbacks, but I didn't want to change the native code without a thumbs up since I'm guessing this is used in one of facebook's apps. Happy to do that if it makes sense I also tried to make the `PERMISSIONS` object a class property - it works in the actual code but not in the documentation (think it's a jsdocs problem), so decided to initialize in the constructor. **Test plan (required)** If the API looks good, I will change the UIExplorer example to use this. cc andreicoman11 Closes https://github.com/facebook/react-native/pull/9292 Differential Revision: D3716303 Pulled By: andreicoman11 fbshipit-source-id: cd40b8757fdf70ea8faecfb58caa00e99a99789e --- .../js/PermissionsExampleAndroid.android.js | 69 +++----- .../PermissionsAndroid/PermissionsAndroid.js | 161 ++++++++++++++++++ Libraries/react-native/react-native.js | 1 + Libraries/react-native/react-native.js.flow | 1 + website/server/extractDocs.js | 1 + 5 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 Libraries/PermissionsAndroid/PermissionsAndroid.js diff --git a/Examples/UIExplorer/js/PermissionsExampleAndroid.android.js b/Examples/UIExplorer/js/PermissionsExampleAndroid.android.js index 4d3a5d96b07be2..129955ba4984ea 100644 --- a/Examples/UIExplorer/js/PermissionsExampleAndroid.android.js +++ b/Examples/UIExplorer/js/PermissionsExampleAndroid.android.js @@ -26,23 +26,22 @@ const React = require('react'); const ReactNative = require('react-native'); const { + PermissionsAndroid, 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.title = 'PermissionsAndroid'; exports.description = 'Permissions example for API 23+.'; class PermissionsExample extends React.Component { state = { - permission: 'android.permission.WRITE_EXTERNAL_STORAGE', + permission: PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, hasPermission: 'Not Checked', }; @@ -63,7 +62,7 @@ class PermissionsExample extends React.Component { Permission Status: {this.state.hasPermission} - + Request Permission @@ -78,51 +77,28 @@ class PermissionsExample extends React.Component { }); }; - _checkPermission = () => { - Permissions.checkPermission( - this.state.permission, - (permission: string, result: boolean) => { - this.setState({ - hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' + permission, - }); - }, - this._showError); - }; - - _shouldExplainPermission = () => { - 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); + _checkPermission = async () => { + let result = await PermissionsAndroid.checkPermission(this.state.permission); + this.setState({ + hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' + + this.state.permission, + }); }; - _requestPermission = () => { - Permissions.requestPermission( + _requestPermission = async () => { + let result = await PermissionsAndroid.requestPermission( this.state.permission, - (permission: string, result: boolean) => { - this.setState({ - hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' + permission, - }); + { + title: 'Permission Explanation', + message: + 'The app needs the following permission ' + this.state.permission + + ' because of reasons. Please approve.' }, - this._showError); - }; - - _showError = () => { - DialogManager.showAlert({message: 'Error'}, {}, {}); + ); + this.setState({ + hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' + + this.state.permission, + }); }; } @@ -150,4 +126,3 @@ var styles = StyleSheet.create({ color: '#007AFF', }, }); - diff --git a/Libraries/PermissionsAndroid/PermissionsAndroid.js b/Libraries/PermissionsAndroid/PermissionsAndroid.js new file mode 100644 index 00000000000000..14797ac0c335e2 --- /dev/null +++ b/Libraries/PermissionsAndroid/PermissionsAndroid.js @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule PermissionsAndroid + * @flow + */ +'use strict'; + +const DialogManagerAndroid = require('NativeModules').DialogManagerAndroid; +const AndroidPermissions = require('NativeModules').AndroidPermissions; + +type Rationale = { + title: string, + message: string, +} + +/** + * `PermissionsAndroid` provides access to Android M's new permissions model. + * Some permissions are granted by default when the application is installed + * so long as they appear in `AndroidManifest.xml`. However, "dangerous" + * permissions require a dialog prompt. You should use this module for those + * permissions. + * + * On devices before SDK version 23, the permissions are automatically granted + * if they appear in the manifest, so `checkPermission` and `requestPermission` + * should always be true. + * + * If a user has previously turned off a permission that you prompt for, the OS + * will advise your app to show a rationale for needing the permission. The + * optional `rationale` argument will show a dialog prompt only if + * necessary - otherwise the normal permission prompt will appear. + * + * ### Example + * ``` + * async function requestCameraPermission() { + * try { + * const granted = await AndroidPermissions.requestPermission( + * AndroidPermissions.PERMISSIONS.CAMERA, + * { + * 'title': 'Cool Photo App Camera Permission', + * 'message': 'Cool Photo App needs access to your camera ' + + * 'so you can take awesome pictures.' + * } + * ) + * if (granted) { + * console.log("You can use the camera") + * } else { + * console.log("Camera permission denied") + * } + * } catch (err) { + * console.warn(err) + * } + * } + * ``` + */ + +class PermissionsAndroid { + PERMISSIONS: Object; + + constructor() { + /** + * A list of specified "dangerous" permissions that require prompting the user + */ + this.PERMISSIONS = { + READ_CALENDAR: 'android.permission.READ_CALENDAR', + WRITE_CALENDAR: 'android.permission.WRITE_CALENDAR', + CAMERA: 'android.permission.CAMERA', + READ_CONTACTS: 'android.permission.READ_CONTACTS', + WRITE_CONTACTS: 'android.permission.WRITE_CONTACTS', + GET_ACCOUNTS: 'android.permission.GET_ACCOUNTS', + ACCESS_FINE_LOCATION: 'android.permission.ACCESS_FINE_LOCATION', + ACCESS_COARSE_LOCATION: 'android.permission.ACCESS_COARSE_LOCATION', + RECORD_AUDIO: 'android.permission.RECORD_AUDIO', + READ_PHONE_STATE: 'android.permission.READ_PHONE_STATE', + CALL_PHONE: 'android.permission.CALL_PHONE', + READ_CALL_LOG: 'android.permission.READ_CALL_LOG', + WRITE_CALL_LOG: 'android.permission.WRITE_CALL_LOG', + ADD_VOICEMAIL: 'com.android.voicemail.permission.ADD_VOICEMAIL', + USE_SIP: 'android.permission.USE_SIP', + PROCESS_OUTGOING_CALLS: 'android.permission.PROCESS_OUTGOING_CALLS', + BODY_SENSORS: 'android.permission.BODY_SENSORS', + SEND_SMS: 'android.permission.SEND_SMS', + RECEIVE_SMS: 'android.permission.RECEIVE_SMS', + READ_SMS: 'android.permission.READ_SMS', + RECEIVE_WAP_PUSH: 'android.permission.RECEIVE_WAP_PUSH', + RECEIVE_MMS: 'android.permission.RECEIVE_MMS', + READ_EXTERNAL_STORAGE: 'android.permission.READ_EXTERNAL_STORAGE', + WRITE_EXTERNAL_STORAGE: 'android.permission.WRITE_EXTERNAL_STORAGE', + }; + } + + /** + * Returns a promise resolving to a boolean value as to whether the specified + * permissions has been granted + */ + checkPermission(permission: string) : Promise { + return new Promise((resolve, reject) => { + AndroidPermissions.checkPermission( + permission, + function(perm: string, result: boolean) { resolve(result); }, + function(error: string) { reject(error); }, + ); + }); + } + + /** + * 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 + * + * 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 + */ + requestPermission(permission: string, rationale?: Rationale) : Promise { + return new Promise((resolve, reject) => { + const requestPermission = () => { + AndroidPermissions.requestPermission( + permission, + function(perm: string, result: boolean) { resolve(result); }, + function(error: string) { reject(error); }, + ); + }; + + if (rationale) { + AndroidPermissions.shouldShowRequestPermissionRationale( + permission, + function(perm: string, shouldShowRationale: boolean) { + if (shouldShowRationale) { + DialogManagerAndroid.showAlert( + rationale, + () => { + DialogManagerAndroid.showAlert({ + message: 'Error Requesting Permissions' + }, {}, {}); + reject(); + }, + requestPermission + ); + } else { + requestPermission(); + } + }, + function(error: string) { reject(error); }, + ); + } else { + requestPermission(); + } + }); + } +} + +PermissionsAndroid = new PermissionsAndroid(); + +module.exports = PermissionsAndroid; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 4f1b5d1b939e17..c3f7f2bcb41486 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -98,6 +98,7 @@ const ReactNative = { get NavigationExperimental() { return require('NavigationExperimental'); }, get NetInfo() { return require('NetInfo'); }, get PanResponder() { return require('PanResponder'); }, + get PermissionsAndroid() { return require('PermissionsAndroid'); }, get PixelRatio() { return require('PixelRatio'); }, get PushNotificationIOS() { return require('PushNotificationIOS'); }, get Settings() { return require('Settings'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 721eb98e378a86..d7c9108e198f74 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -110,6 +110,7 @@ var ReactNative = { NavigationExperimental: require('NavigationExperimental'), NetInfo: require('NetInfo'), PanResponder: require('PanResponder'), + PermissionsAndroid: require('PermissionsAndroid'), PixelRatio: require('PixelRatio'), PushNotificationIOS: require('PushNotificationIOS'), Settings: require('Settings'), diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 3540cdcfb224d6..ba0a41ecce5137 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -550,6 +550,7 @@ const apis = [ '../Libraries/BatchedBridge/BatchedBridgedModules/NativeModules.js', '../Libraries/Network/NetInfo.js', '../Libraries/Interaction/PanResponder.js', + '../Libraries/PermissionsAndroid/PermissionsAndroid.js', '../Libraries/Utilities/PixelRatio.js', '../Libraries/PushNotificationIOS/PushNotificationIOS.js', '../Libraries/Settings/Settings.ios.js',