Skip to content

Commit 51efaab

Browse files
cmcewenFacebook Github Bot
authored and
Facebook Github Bot
committed
Handle "Never Ask Again" in permissions and add requestMultiplePermissions
Summary: In order to get featured in the Google Play Store, we had to handle a few specific cases with permissions based on feedback from the editorial team. First, which was previously possible with this permissions module was bumping the sdk to version 23. The second is requesting multiple permissions at one time. In order for the camera + upload to work, we needed to request both camera permissions + media storage in one flow. The last is handling the case where the user checks the "Never Ask Again" box. This will only appear after a user denies a permission once and is then prompted again. The logic for handling this case is taken from here: http://stackoverflow.com/questions/31928868/how-do-we-distinguish-never-asked-from-stop-asking-in-android-ms-runtime-permis/35495372#35495372 We were also seeing a few crashes similar to #10009 due to `onRequestPermissionsResult` being called before `onResume` (http://stackoverflow.com/questions/35205643/why-is-onresume-called-after-onrequestpermissionsresult), so I delaye Closes #10221 Differential Revision: D4232551 fbshipit-source-id: fee698d1c48a2d86623cb87996f3d17f4c10a62e
1 parent 5d30045 commit 51efaab

File tree

6 files changed

+186
-34
lines changed

6 files changed

+186
-34
lines changed

Examples/UIExplorer/android/app/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ android {
8989
defaultConfig {
9090
applicationId "com.facebook.react.uiapp"
9191
minSdkVersion 16
92-
targetSdkVersion 22
92+
targetSdkVersion 23
9393
versionCode 1
9494
versionName "1.0"
9595
ndk {

Examples/UIExplorer/android/app/src/main/AndroidManifest.xml

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
1212
<uses-permission android:name="android.permission.VIBRATE"/>
1313

14+
<!--Just to show permissions example-->
15+
<uses-permission android:name="android.permission.CAMERA"/>
16+
<uses-permission android:name="android.permission.READ_CALENDAR"/>
17+
1418
<uses-sdk
1519
android:minSdkVersion="16"
1620
android:targetSdkVersion="23" />

Examples/UIExplorer/js/PermissionsExampleAndroid.android.js

+21-14
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,38 @@ const React = require('react');
2727
const ReactNative = require('react-native');
2828
const {
2929
PermissionsAndroid,
30+
Picker,
3031
StyleSheet,
3132
Text,
32-
TextInput,
3333
TouchableWithoutFeedback,
3434
View,
3535
} = ReactNative;
3636

37+
const Item = Picker.Item;
38+
3739
exports.displayName = (undefined: ?string);
3840
exports.framework = 'React';
3941
exports.title = 'PermissionsAndroid';
4042
exports.description = 'Permissions example for API 23+.';
4143

4244
class PermissionsExample extends React.Component {
4345
state = {
44-
permission: PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
46+
permission: PermissionsAndroid.PERMISSIONS.CAMERA,
4547
hasPermission: 'Not Checked',
4648
};
4749

4850
render() {
4951
return (
5052
<View style={styles.container}>
5153
<Text style={styles.text}>Permission Name:</Text>
52-
<TextInput
53-
autoFocus={true}
54-
autoCorrect={false}
55-
style={styles.singleLine}
56-
onChange={this._updateText}
57-
defaultValue={this.state.permission}
58-
/>
54+
<Picker
55+
style={styles.picker}
56+
selectedValue={this.state.permission}
57+
onValueChange={this._onSelectPermission.bind(this)}>
58+
<Item label={PermissionsAndroid.PERMISSIONS.CAMERA} value={PermissionsAndroid.PERMISSIONS.CAMERA} />
59+
<Item label={PermissionsAndroid.PERMISSIONS.READ_CALENDAR} value={PermissionsAndroid.PERMISSIONS.READ_CALENDAR} />
60+
<Item label={PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION} value={PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION} />
61+
</Picker>
5962
<TouchableWithoutFeedback onPress={this._checkPermission}>
6063
<View>
6164
<Text style={[styles.touchable, styles.text]}>Check Permission</Text>
@@ -71,22 +74,22 @@ class PermissionsExample extends React.Component {
7174
);
7275
}
7376

74-
_updateText = (event: Object) => {
77+
_onSelectPermission = (permission: string) => {
7578
this.setState({
76-
permission: event.nativeEvent.text,
79+
permission: permission,
7780
});
7881
};
7982

8083
_checkPermission = async () => {
81-
let result = await PermissionsAndroid.checkPermission(this.state.permission);
84+
let result = await PermissionsAndroid.check(this.state.permission);
8285
this.setState({
8386
hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' +
8487
this.state.permission,
8588
});
8689
};
8790

8891
_requestPermission = async () => {
89-
let result = await PermissionsAndroid.requestPermission(
92+
let result = await PermissionsAndroid.request(
9093
this.state.permission,
9194
{
9295
title: 'Permission Explanation',
@@ -95,8 +98,9 @@ class PermissionsExample extends React.Component {
9598
' because of reasons. Please approve.'
9699
},
97100
);
101+
98102
this.setState({
99-
hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' +
103+
hasPermission: result + ' for ' +
100104
this.state.permission,
101105
});
102106
};
@@ -125,4 +129,7 @@ var styles = StyleSheet.create({
125129
touchable: {
126130
color: '#007AFF',
127131
},
132+
picker: {
133+
flex: 1,
134+
}
128135
});

Libraries/PermissionsAndroid/PermissionsAndroid.js

+52-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type Rationale = {
1919
message: string;
2020
}
2121

22+
type PermissionStatus = 'granted' | 'denied' | 'never_ask_again';
23+
2224
/**
2325
* `PermissionsAndroid` provides access to Android M's new permissions model.
2426
* Some permissions are granted by default when the application is installed
@@ -47,7 +49,7 @@ type Rationale = {
4749
* 'so you can take awesome pictures.'
4850
* }
4951
* )
50-
* if (granted) {
52+
* if (granted === PermissionsAndroid.RESULTS.GRANTED) {
5153
* console.log("You can use the camera")
5254
* } else {
5355
* console.log("Camera permission denied")
@@ -61,6 +63,7 @@ type Rationale = {
6163

6264
class PermissionsAndroid {
6365
PERMISSIONS: Object;
66+
RESULTS: Object;
6467

6568
constructor() {
6669
/**
@@ -92,17 +95,38 @@ class PermissionsAndroid {
9295
READ_EXTERNAL_STORAGE: 'android.permission.READ_EXTERNAL_STORAGE',
9396
WRITE_EXTERNAL_STORAGE: 'android.permission.WRITE_EXTERNAL_STORAGE',
9497
};
98+
99+
this.RESULTS = {
100+
GRANTED: 'granted',
101+
DENIED: 'denied',
102+
NEVER_ASK_AGAIN: 'never_ask_again',
103+
};
95104
}
96105

97106
/**
107+
* DEPRECATED - use check
108+
*
98109
* Returns a promise resolving to a boolean value as to whether the specified
99110
* permissions has been granted
111+
*
112+
* @deprecated
100113
*/
101114
checkPermission(permission: string) : Promise<boolean> {
115+
console.warn('"PermissionsAndroid.checkPermission" is deprecated. Use "PermissionsAndroid.check" instead');
102116
return Permissions.checkPermission(permission);
103117
}
104118

105119
/**
120+
* Returns a promise resolving to a boolean value as to whether the specified
121+
* permissions has been granted
122+
*/
123+
check(permission: string) : Promise<boolean> {
124+
return Permissions.checkPermission(permission);
125+
}
126+
127+
/**
128+
* DEPRECATED - use request
129+
*
106130
* Prompts the user to enable a permission and returns a promise resolving to a
107131
* boolean value indicating whether the user allowed or denied the request
108132
*
@@ -111,8 +135,26 @@ class PermissionsAndroid {
111135
* necessary to show a dialog explaining why the permission is needed
112136
* (https://developer.android.com/training/permissions/requesting.html#explain)
113137
* and then shows the system permission dialog
138+
*
139+
* @deprecated
114140
*/
115141
async requestPermission(permission: string, rationale?: Rationale) : Promise<boolean> {
142+
console.warn('"PermissionsAndroid.requestPermission" is deprecated. Use "PermissionsAndroid.request" instead');
143+
const response = await this.request(permission, rationale);
144+
return (response === this.RESULTS.GRANTED);
145+
}
146+
147+
/**
148+
* Prompts the user to enable a permission and returns a promise resolving to a
149+
* string value indicating whether the user allowed or denied the request
150+
*
151+
* If the optional rationale argument is included (which is an object with a
152+
* `title` and `message`), this function checks with the OS whether it is
153+
* necessary to show a dialog explaining why the permission is needed
154+
* (https://developer.android.com/training/permissions/requesting.html#explain)
155+
* and then shows the system permission dialog
156+
*/
157+
async request(permission: string, rationale?: Rationale) : Promise<PermissionStatus> {
116158
if (rationale) {
117159
const shouldShowRationale = await Permissions.shouldShowRequestPermissionRationale(permission);
118160

@@ -128,6 +170,15 @@ class PermissionsAndroid {
128170
}
129171
return Permissions.requestPermission(permission);
130172
}
173+
174+
/**
175+
* Prompts the user to enable multiple permissions in the same dialog and
176+
* returns an object with the permissions as keys and strings as values
177+
* indicating whether the user allowed or denied the request
178+
*/
179+
requestMultiple(permissions: Array<string>) : Promise<{[permission: string]: PermissionStatus}> {
180+
return Permissions.requestMultiplePermissions(permissions);
181+
}
131182
}
132183

133184
PermissionsAndroid = new PermissionsAndroid();

ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java

+21-9
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
package com.facebook.react;
44

5-
import javax.annotation.Nullable;
6-
75
import android.annotation.TargetApi;
86
import android.app.Activity;
97
import android.content.Context;
@@ -17,11 +15,14 @@
1715

1816
import com.facebook.common.logging.FLog;
1917
import com.facebook.infer.annotation.Assertions;
18+
import com.facebook.react.bridge.Callback;
2019
import com.facebook.react.common.ReactConstants;
2120
import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
2221
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
2322
import com.facebook.react.modules.core.PermissionListener;
2423

24+
import javax.annotation.Nullable;
25+
2526
/**
2627
* Delegate class for {@link ReactActivity} and {@link ReactFragmentActivity}. You can subclass this
2728
* to provide custom implementations for e.g. {@link #getReactNativeHost()}, if your Application
@@ -38,6 +39,7 @@ public class ReactActivityDelegate {
3839
private @Nullable ReactRootView mReactRootView;
3940
private @Nullable DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
4041
private @Nullable PermissionListener mPermissionListener;
42+
private @Nullable Callback mPermissionsCallback;
4143

4244
public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
4345
mActivity = activity;
@@ -117,6 +119,11 @@ protected void onResume() {
117119
getPlainActivity(),
118120
(DefaultHardwareBackBtnHandler) getPlainActivity());
119121
}
122+
123+
if (mPermissionsCallback != null) {
124+
mPermissionsCallback.invoke();
125+
mPermissionsCallback = null;
126+
}
120127
}
121128

122129
protected void onDestroy() {
@@ -178,13 +185,18 @@ public void requestPermissions(
178185
}
179186

180187
public void onRequestPermissionsResult(
181-
int requestCode,
182-
String[] permissions,
183-
int[] grantResults) {
184-
if (mPermissionListener != null &&
185-
mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
186-
mPermissionListener = null;
187-
}
188+
final int requestCode,
189+
final String[] permissions,
190+
final int[] grantResults) {
191+
mPermissionsCallback = new Callback() {
192+
@Override
193+
public void invoke(Object... args) {
194+
if (mPermissionListener != null &&
195+
mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
196+
mPermissionListener = null;
197+
}
198+
}
199+
};
188200
}
189201

190202
private Context getContext() {

0 commit comments

Comments
 (0)