Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PermissionsAndroid] Handle "Never Ask Again" in permissions and add requestMultiplePermissions #10221

Closed
wants to merge 10 commits into from
2 changes: 1 addition & 1 deletion Examples/UIExplorer/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ android {
defaultConfig {
applicationId "com.facebook.react.uiapp"
minSdkVersion 16
targetSdkVersion 22
targetSdkVersion 23
versionCode 1
versionName "1.0"
ndk {
Expand Down
4 changes: 4 additions & 0 deletions Examples/UIExplorer/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>

<!--Just to show permissions example-->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/>

<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="23" />
Expand Down
35 changes: 21 additions & 14 deletions Examples/UIExplorer/js/PermissionsExampleAndroid.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,38 @@ 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';
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',
};

render() {
return (
<View style={styles.container}>
<Text style={styles.text}>Permission Name:</Text>
<TextInput
autoFocus={true}
autoCorrect={false}
style={styles.singleLine}
onChange={this._updateText}
defaultValue={this.state.permission}
/>
<Picker
style={styles.picker}
selectedValue={this.state.permission}
onValueChange={this._onSelectPermission.bind(this)}>
<Item label={PermissionsAndroid.PERMISSIONS.CAMERA} value={PermissionsAndroid.PERMISSIONS.CAMERA} />
<Item label={PermissionsAndroid.PERMISSIONS.READ_CALENDAR} value={PermissionsAndroid.PERMISSIONS.READ_CALENDAR} />
<Item label={PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION} value={PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION} />
</Picker>
<TouchableWithoutFeedback onPress={this._checkPermission}>
<View>
<Text style={[styles.touchable, styles.text]}>Check Permission</Text>
Expand All @@ -71,22 +74,22 @@ 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.query(this.state.permission);
this.setState({
hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' +
this.state.permission,
});
};

_requestPermission = async () => {
let result = await PermissionsAndroid.requestPermission(
let result = await PermissionsAndroid.request(
this.state.permission,
{
title: 'Permission Explanation',
Expand All @@ -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,
});
};
Expand Down Expand Up @@ -125,4 +129,7 @@ var styles = StyleSheet.create({
touchable: {
color: '#007AFF',
},
picker: {
flex: 1,
}
});
51 changes: 50 additions & 1 deletion Libraries/PermissionsAndroid/PermissionsAndroid.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,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")
Expand All @@ -61,6 +61,7 @@ type Rationale = {

class PermissionsAndroid {
PERMISSIONS: Object;
RESULTS: Object;

constructor() {
/**
Expand Down Expand Up @@ -92,17 +93,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 query
*
* Returns a promise resolving to a boolean value as to whether the specified
* permissions has been granted
*
* @deprecated
*/
checkPermission(permission: string) : Promise<boolean> {
console.warn('"PermissionsAndroid.checkPermission" is deprecated. Use "PermissionsAndroid.query" instead');
return Permissions.checkPermission(permission);
}

/**
* Returns a promise resolving to a boolean value as to whether the specified
* permissions has been granted
*/
query(permission: string) : Promise<boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to change this to check since it doesn't actually return the permission status (which is what I thought at first)

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
*
Expand All @@ -111,8 +133,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<boolean> {
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<string> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add type PermissionStatus = 'granted' | 'denied' | 'never_ask_again' at the top and change Promise<string> to Promise<PermissionStatus>

if (rationale) {
const shouldShowRationale = await Permissions.shouldShowRequestPermissionRationale(permission);

Expand All @@ -128,6 +168,15 @@ class PermissionsAndroid {
}
return Permissions.requestPermission(permission);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change flow return type from Promise<boolean> to Promise<string>

Will at least warn people using flow that this api has changed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although because this is javascript, if (granted) { will just evaluate as true if granted is a non empty string. So we won't get a a flow warning.

}

/**
* 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<string>) : Promise<{[permission: string]: string}> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change Promise<{[permission: string]: PermissionStatus}>

return Permissions.requestMultiplePermissions(permissions);
}
}

PermissionsAndroid = new PermissionsAndroid();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

package com.facebook.react;

import javax.annotation.Nullable;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
Expand All @@ -17,11 +15,14 @@

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
Expand All @@ -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;
Expand Down Expand Up @@ -117,6 +119,11 @@ protected void onResume() {
getPlainActivity(),
(DefaultHardwareBackBtnHandler) getPlainActivity());
}

if (mPermissionsCallback != null) {
mPermissionsCallback.invoke();
mPermissionsCallback = null;
}
}

protected void onDestroy() {
Expand Down Expand Up @@ -178,13 +185,18 @@ public void requestPermissions(
}

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() {
Expand Down
Loading