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

feat(android): switch to new callback-style permission requests #4033

Merged
merged 25 commits into from Jan 8, 2021
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
14e644c
Uses new permissions callbacks, work so far
carlpoole Dec 21, 2020
08ebc98
begun moving permission caching work to new system, reorganizing
carlpoole Dec 23, 2020
73526da
added PermissionState back after reconciling with reverted commit in …
carlpoole Dec 23, 2020
4fbc641
added PermissionState class to represent states instead of raw strings
carlpoole Dec 23, 2020
8bb79ec
updated requestPermissions call to handle aliases and pass through to…
carlpoole Dec 23, 2020
773ebec
undoing unintentional iOS format change
carlpoole Dec 23, 2020
a563f25
Update CAPBridgeViewController.swift
carlpoole Dec 23, 2020
d000384
Merge branch 'main' into perms-1.2
carlpoole Dec 29, 2020
1f8f848
protected for access, might change this. PluginMethod annotation went…
carlpoole Dec 30, 2020
e9b5ee2
check if perm result handled by plugin before OS handles callback
carlpoole Jan 4, 2021
a657476
Merge branch 'perms-1.2' of github.com:ionic-team/capacitor into perm…
carlpoole Jan 4, 2021
1927042
annotated permission response methods
carlpoole Jan 7, 2021
d894e3b
remove unused Permissionallback
carlpoole Jan 7, 2021
d6db63f
removed PermissionResponse annotation and merged into PluginMethod
carlpoole Jan 7, 2021
7e3e6dc
improved default behavior for overriden permissionRequest
carlpoole Jan 7, 2021
6b44c59
Merge branch 'main' into perms-1.2
carlpoole Jan 7, 2021
8b77ed1
Undoing iOS fmt change that happened
carlpoole Jan 7, 2021
419456b
removed permissionRequestCode from CapacitorPlugin annotiation for v3
carlpoole Jan 7, 2021
6b95ed2
removed unused annotation
carlpoole Jan 8, 2021
4624228
Comment improvements, cleanup
carlpoole Jan 8, 2021
5751e53
hard reject when permissions requested and no registered callback, co…
carlpoole Jan 8, 2021
d209086
refactor getPluginWithRequestCode
carlpoole Jan 8, 2021
7fbcf60
Merge branch 'main' into perms-1.2
carlpoole Jan 8, 2021
2011ed3
Merge branch 'main' into perms-1.2
carlpoole Jan 8, 2021
f2b86a7
Merge branch 'main' into perms-1.2
imhoffd Jan 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/capacitor/build.gradle
Expand Up @@ -59,6 +59,8 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.core:core:$androidxCoreVersion"
implementation 'androidx.activity:activity:1.2.0-rc01'
implementation 'androidx.fragment:fragment:1.3.0-rc01'
implementation "com.google.android.material:material:$androidxMaterialVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
Expand Down
106 changes: 46 additions & 60 deletions android/capacitor/src/main/java/com/getcapacitor/Bridge.java
Expand Up @@ -7,7 +7,6 @@
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Color;
import android.net.Uri;
Expand Down Expand Up @@ -488,7 +487,6 @@ public PluginHandle getPlugin(String pluginId) {
public PluginHandle getPluginWithRequestCode(int requestCode) {
for (PluginHandle handle : this.plugins.values()) {
int[] requestCodes;
int permissionRequestCode;

CapacitorPlugin pluginAnnotation = handle.getPluginAnnotation();
if (pluginAnnotation == null) {
Expand All @@ -498,22 +496,20 @@ public PluginHandle getPluginWithRequestCode(int requestCode) {
continue;
}

if (legacyPluginAnnotation.permissionRequestCode() == requestCode) {
return handle;
}

requestCodes = legacyPluginAnnotation.requestCodes();
permissionRequestCode = legacyPluginAnnotation.permissionRequestCode();
} else {
requestCodes = pluginAnnotation.requestCodes();
permissionRequestCode = pluginAnnotation.permissionRequestCode();
}

for (int rc : requestCodes) {
if (rc == requestCode) {
return handle;
}
}

if (permissionRequestCode == requestCode) {
return handle;
}
}
return null;
}
Expand Down Expand Up @@ -760,47 +756,35 @@ public void startActivityForPluginWithResult(PluginCall call, Intent intent, int
}

/**
* Handle a request permission result by finding the that requested
* the permission and calling their permission handler
* Check for legacy Capacitor or Cordova plugins that may have registered to handle a permission
imhoffd marked this conversation as resolved.
Show resolved Hide resolved
* request, and handle them if so. If not handled, false is returned.
*
* @param requestCode the code that was requested
* @param permissions the permissions requested
* @param grantResults the set of granted/denied permissions
* @return true if permission code was handled by a plugin explicitly, false if not
imhoffd marked this conversation as resolved.
Show resolved Hide resolved
*/
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
PluginHandle plugin = getPluginWithRequestCode(requestCode);

if (plugin == null) {
boolean permissionHandled = false;
Logger.debug("Unable to find a Capacitor plugin to handle permission requestCode, trying Cordova plugins " + requestCode);
try {
cordovaInterface.onRequestPermissionResult(requestCode, permissions, grantResults);
permissionHandled = cordovaInterface.handlePermissionResult(requestCode, permissions, grantResults);
} catch (JSONException e) {
Logger.debug("Error on Cordova plugin permissions request " + e.getMessage());
}
return;
return permissionHandled;
}

if (plugin.getPluginAnnotation() != null) {
// Handle for @CapacitorPlugin permissions
PluginCall savedPermissionCall = getPermissionCall(plugin.getId());
if (savedPermissionCall != null) {
if (validatePermissions(plugin.getInstance(), savedPermissionCall, permissions, grantResults)) {
// handle request permissions call
if (savedPermissionCall.getMethodName().equals("requestPermissions")) {
savedPermissionCall.resolve(plugin.getInstance().getPermissionStates());
} else {
// handle permission requests by other methods on the plugin
plugin.getInstance().onRequestPermissionsResult(savedPermissionCall, requestCode, permissions, grantResults);

if (!savedPermissionCall.isReleased() && !savedPermissionCall.isSaved()) {
savedPermissionCall.release(this);
}
}
}
}
} else {
// Call deprecated method if using deprecated NativePlugin annotation
// Call deprecated method if using deprecated NativePlugin annotation
if (plugin.getPluginAnnotation() == null) {
plugin.getInstance().handleRequestPermissionsResult(requestCode, permissions, grantResults);
return true;
}

return false;
}

/**
Expand All @@ -810,43 +794,45 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in
* @param plugin
* @param savedCall
* @param permissions
* @param grantResults
* @return true if permissions were saved and defined correctly, false if not
*/
protected boolean validatePermissions(Plugin plugin, PluginCall savedCall, String[] permissions, int[] grantResults) {
protected boolean validatePermissions(Plugin plugin, PluginCall savedCall, Map<String, Boolean> permissions) {
SharedPreferences prefs = getContext().getSharedPreferences(PERMISSION_PREFS_NAME, Activity.MODE_PRIVATE);

if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted. If previously denied, remove cached state
for (String permission : permissions) {
String state = prefs.getString(permission, null);
for (Map.Entry<String, Boolean> permission : permissions.entrySet()) {
String permString = permission.getKey();
boolean isGranted = permission.getValue();

if (isGranted) {
// Permission granted. If previously denied, remove cached state
String state = prefs.getString(permString, null);

if (state != null) {
SharedPreferences.Editor editor = prefs.edit();
editor.remove(permission);
editor.remove(permString);
editor.apply();
}
}
} else {
for (String permission : permissions) {
} else {
SharedPreferences.Editor editor = prefs.edit();

if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permission)) {
if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permString)) {
// Permission denied, can prompt again with rationale
editor.putString(permission, "prompt-with-rationale");
editor.putString(permString, PermissionState.PROMPT_WITH_RATIONALE.toString());
} else {
// Permission denied permanently, store this state for future reference
editor.putString(permission, "denied");
editor.putString(permString, PermissionState.DENIED.toString());
}

editor.apply();
}
}

if (!plugin.hasDefinedPermissions(permissions)) {
String[] permStrings = permissions.keySet().toArray(new String[0]);

if (!plugin.hasDefinedPermissions(permStrings)) {
StringBuilder builder = new StringBuilder();
builder.append("Missing the following permissions in AndroidManifest.xml:\n");
String[] missing = PermissionHelper.getUndefinedPermissions(getContext(), permissions);
String[] missing = PermissionHelper.getUndefinedPermissions(getContext(), permStrings);
for (String perm : missing) {
builder.append(perm + "\n");
}
Expand All @@ -861,43 +847,43 @@ protected boolean validatePermissions(Plugin plugin, PluginCall savedCall, Strin
* Helper to check all permissions and see the current states of each permission.
*
* @since 3.0.0
* @return A mapping of permissions to the associated granted status.
* @return A mapping of permission aliases to the associated granted status.
*/
protected JSObject getPermissionStates(Plugin plugin) {
JSObject permissionsResults = new JSObject();
protected Map<String, PermissionState> getPermissionStates(Plugin plugin) {
Map<String, PermissionState> permissionsResults = new HashMap<>();
CapacitorPlugin annotation = plugin.getPluginHandle().getPluginAnnotation();
for (Permission perm : annotation.permissions()) {
// If a permission is defined with no permission constants, return "granted" for it.
// If a permission is defined with no permission constants, return GRANTED for it.
// Otherwise, get its true state.
if (perm.strings().length == 0 || (perm.strings().length == 1 && perm.strings()[0].isEmpty())) {
String key = perm.alias();
if (!key.isEmpty()) {
String existingResult = permissionsResults.getString(key);
PermissionState existingResult = permissionsResults.get(key);

// auto set permission state to granted if the alias is empty.
// auto set permission state to GRANTED if the alias is empty.
if (existingResult == null) {
permissionsResults.put(key, "granted");
permissionsResults.put(key, PermissionState.GRANTED);
}
}
} else {
for (String permString : perm.strings()) {
String key = perm.alias().isEmpty() ? permString : perm.alias();
String permissionStatus = plugin.hasPermission(permString) ? "granted" : "prompt";
PermissionState permissionStatus = plugin.hasPermission(permString) ? PermissionState.GRANTED : PermissionState.PROMPT;

// Check if there is a cached permission state for the "Never ask again" state
if (permissionStatus.equals("prompt")) {
if (permissionStatus == PermissionState.PROMPT) {
SharedPreferences prefs = getContext().getSharedPreferences(PERMISSION_PREFS_NAME, Activity.MODE_PRIVATE);
String state = prefs.getString(permString, null);

if (state != null) {
permissionStatus = state;
permissionStatus = PermissionState.valueOf(state);
}
}

String existingResult = permissionsResults.getString(key);
PermissionState existingResult = permissionsResults.get(key);

// multiple permissions with the same alias must all be true, otherwise all false.
if (existingResult == null || existingResult.equals("granted")) {
if (existingResult == null || existingResult == PermissionState.GRANTED) {
permissionsResults.put(key, permissionStatus);
}
}
Expand Down
Expand Up @@ -164,13 +164,31 @@ public void onDetachedFromWindow() {
this.bridge.onDetachedFromWindow();
}

/**
* Handles permission request results.
*
* Capacitor is backwards compatible such that plugins using legacy permission request codes
* may coexist with plugins using the AndroidX Activity v1.2 permission callback flow introduced
* in Capacitor 3.0.
*
* In this method, plugins are checked first for ownership of the legacy permission request code.
* If the {@link Bridge#onRequestPermissionsResult(int, String[], int[])} method indicates it has
* handled the permission, then the permission callback will be considered complete. Otherwise,
* the permission will be handled using the AndroidX Activity flow.
*
* @param requestCode the request code associated with the permission request
* @param permissions the Android permission strings requested
* @param grantResults the status result of the permission request
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (this.bridge == null) {
return;
}

this.bridge.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (!bridge.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

@Override
Expand Down
@@ -0,0 +1,24 @@
package com.getcapacitor;

/**
* Represents the state of a permission
*
* @since 3.0.0
*/
public enum PermissionState {
GRANTED("granted"),
DENIED("denied"),
PROMPT("prompt"),
PROMPT_WITH_RATIONALE("prompt-with-rationale");

private String state;

PermissionState(String state) {
this.state = state;
}

@Override
public String toString() {
return state;
}
}