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
28 changes: 17 additions & 11 deletions Examples/UIExplorer/js/PermissionsExampleAndroid.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -49,13 +51,14 @@ class PermissionsExample extends React.Component {
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,9 +74,9 @@ class PermissionsExample extends React.Component {
);
}

_updateText = (event: Object) => {
_onSelectPermission = (permission: string) => {
this.setState({
permission: event.nativeEvent.text,
permission: permission,
});
};

Expand All @@ -96,7 +99,7 @@ class PermissionsExample extends React.Component {
},
);
this.setState({
hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' +
hasPermission: result + ' for ' +
this.state.permission,
});
};
Expand Down Expand Up @@ -125,4 +128,7 @@ var styles = StyleSheet.create({
touchable: {
color: '#007AFF',
},
picker: {
flex: 1,
}
});
20 changes: 18 additions & 2 deletions 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.PERMISSION_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,6 +93,12 @@ class PermissionsAndroid {
READ_EXTERNAL_STORAGE: 'android.permission.READ_EXTERNAL_STORAGE',
WRITE_EXTERNAL_STORAGE: 'android.permission.WRITE_EXTERNAL_STORAGE',
};

this.RESULTS = {
PERMISSION_GRANTED: 'PERMISSION_GRANTED',
PERMISSION_DENIED: 'PERMISSION_DENIED',
PERMISSION_NEVER_ASK_AGAIN: 'PERMISSION_NEVER_ASK_AGAIN',
};
}

/**
Expand All @@ -104,7 +111,7 @@ class PermissionsAndroid {

/**
* 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
* 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
Expand All @@ -128,6 +135,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
*/
requestMultiplePermissions(permissions: Array<string>) : Promise<Object> {
Copy link
Contributor

@AndrewJack AndrewJack Oct 19, 2016

Choose a reason for hiding this comment

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

Specify a stricter flow return type:

{ [permission: string]: string }

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
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@
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.
*/
Expand All @@ -32,6 +37,9 @@ public class PermissionsModule extends ReactContextBaseJavaModule implements Per

private final SparseArray<Callback> mCallbacks;
private int mRequestCode = 0;
private final String PERMISSION_GRANTED = "PERMISSION_GRANTED";
private final String PERMISSION_DENIED = "PERMISSION_DENIED";
private final String PERMISSION_NEVER_ASK_AGAIN = "PERMISSION_NEVER_ASK_AGAIN";

public PermissionsModule(ReactApplicationContext reactContext) {
super(reactContext);
Expand All @@ -51,8 +59,7 @@ public String getName() {
public void checkPermission(final String permission, final Promise promise) {
PermissionAwareActivity activity = getPermissionAwareActivity();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
promise.resolve(activity.checkPermission(permission, Process.myPid(), Process.myUid()) ==
PackageManager.PERMISSION_GRANTED);
promise.resolve(activity.checkPermission(permission, Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED);
return;
}
promise.resolve(activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED);
Expand Down Expand Up @@ -84,39 +91,103 @@ public void shouldShowRequestPermissionRationale(final String permission, final
@ReactMethod
public void requestPermission(final String permission, final Promise promise) {
PermissionAwareActivity activity = getPermissionAwareActivity();

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
promise.resolve(activity.checkPermission(permission, Process.myPid(), Process.myUid()) ==
PackageManager.PERMISSION_GRANTED);
PackageManager.PERMISSION_GRANTED ? PERMISSION_GRANTED : PERMISSION_DENIED);
return;
}
if (activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
promise.resolve(true);
promise.resolve(PERMISSION_GRANTED);
return;
}

mCallbacks.put(
mRequestCode, new Callback() {
@Override
public void invoke(Object... args) {
promise.resolve(args[0].equals(PackageManager.PERMISSION_GRANTED));
mRequestCode, new Callback() {
@Override
public void invoke(Object... args) {
int[] results = (int[]) args[0];
if (results[0] == PackageManager.PERMISSION_GRANTED) {
promise.resolve(PERMISSION_GRANTED);
} else {
PermissionAwareActivity activity = (PermissionAwareActivity) args[1];
if (activity.shouldShowRequestPermissionRationale(permission)) {
promise.resolve(PERMISSION_DENIED);
} else {
promise.resolve(PERMISSION_NEVER_ASK_AGAIN);
}
}
});
}
});

activity.requestPermissions(new String[]{permission}, mRequestCode, this);
mRequestCode++;
}

@ReactMethod
public void requestMultiplePermissions(final ReadableArray permissions, final Promise promise) {
PermissionAwareActivity activity = getPermissionAwareActivity();
final WritableMap grantedPermissions = new WritableNativeMap();
final ArrayList<String> permissionsToCheck = new ArrayList<String>();
int checkedPermissionsCount = 0;

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, activity.checkPermission(perm, Process.myPid(), Process.myUid()) ==
PackageManager.PERMISSION_GRANTED ? PERMISSION_GRANTED : PERMISSION_DENIED);
checkedPermissionsCount++;
} else if (activity.checkSelfPermission(perm) == PackageManager.PERMISSION_GRANTED) {
grantedPermissions.putString(perm, PERMISSION_GRANTED);
checkedPermissionsCount++;
} else {
permissionsToCheck.add(perm);
}
}
if (permissions.size() == checkedPermissionsCount) {
promise.resolve(grantedPermissions);
return;
}


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, PERMISSION_GRANTED);
} else {
if (activity.shouldShowRequestPermissionRationale(permission)) {
grantedPermissions.putString(permission, PERMISSION_DENIED);
} else {
grantedPermissions.putString(permission, PERMISSION_NEVER_ASK_AGAIN);
}
}
}
promise.resolve(grantedPermissions);
}
});

activity.requestPermissions(permissionsToCheck.toArray(new String[0]), 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;
int requestCode,
String[] permissions,
int[] grantResults) {
mCallbacks.get(requestCode).invoke(grantResults, getPermissionAwareActivity());
mCallbacks.remove(requestCode);
return mCallbacks.size() == 0;
}

private PermissionAwareActivity getPermissionAwareActivity() {
Expand Down