From eec61a6d8d8edfe94aea1a361787d1e6c736e20d Mon Sep 17 00:00:00 2001 From: Carl Poole Date: Fri, 30 Oct 2020 13:06:46 -0500 Subject: [PATCH] feat: improve permissions Co-authored-by: Dan Imhoff Co-authored-by: jcesarmobile --- .../main/java/com/getcapacitor/Bridge.java | 50 +++- .../getcapacitor/BridgeWebChromeClient.java | 48 +-- .../java/com/getcapacitor/NativePlugin.java | 4 + .../main/java/com/getcapacitor/Plugin.java | 275 +++++++++++++----- .../java/com/getcapacitor/PluginHandle.java | 39 ++- .../annotation/CapacitorPlugin.java | 33 +++ .../getcapacitor/annotation/Permission.java | 22 ++ .../getcapacitor/util/PermissionHelper.java | 98 +++++++ core/src/definitions.ts | 9 +- core/src/web/index.ts | 18 +- 10 files changed, 439 insertions(+), 157 deletions(-) create mode 100644 android/capacitor/src/main/java/com/getcapacitor/annotation/CapacitorPlugin.java create mode 100644 android/capacitor/src/main/java/com/getcapacitor/annotation/Permission.java create mode 100644 android/capacitor/src/main/java/com/getcapacitor/util/PermissionHelper.java diff --git a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java index 646387c619..4ad07395d1 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java +++ b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java @@ -15,6 +15,7 @@ import android.webkit.ValueCallback; import android.webkit.WebSettings; import android.webkit.WebView; +import com.getcapacitor.annotation.CapacitorPlugin; import com.getcapacitor.plugin.SplashScreen; import com.getcapacitor.util.HostMask; import java.io.File; @@ -403,18 +404,27 @@ public void registerPlugins(Class[] pluginClasses) { * @param pluginClass a class inheriting from Plugin */ public void registerPlugin(Class pluginClass) { - NativePlugin pluginAnnotation = pluginClass.getAnnotation(NativePlugin.class); + String pluginName; + CapacitorPlugin pluginAnnotation = pluginClass.getAnnotation(CapacitorPlugin.class); if (pluginAnnotation == null) { - Logger.error("NativePlugin doesn't have the @NativePlugin annotation. Please add it"); - return; + NativePlugin legacyPluginAnnotation = pluginClass.getAnnotation(NativePlugin.class); + + if (legacyPluginAnnotation == null) { + Logger.error("Plugin doesn't have the @CapacitorPlugin annotation. Please add it"); + return; + } + + pluginName = legacyPluginAnnotation.name(); + } else { + pluginName = pluginAnnotation.name(); } String pluginId = pluginClass.getSimpleName(); // Use the supplied name as the id if available - if (!pluginAnnotation.name().equals("")) { - pluginId = pluginAnnotation.name(); + if (!pluginName.equals("")) { + pluginId = pluginName; } Logger.debug("Registering plugin: " + pluginId); @@ -425,7 +435,7 @@ public void registerPlugin(Class pluginClass) { Logger.error( "NativePlugin " + pluginClass.getName() + - " is invalid. Ensure the @NativePlugin annotation exists on the plugin class and" + + " is invalid. Ensure the @CapacitorPlugin annotation exists on the plugin class and" + " the class extends Plugin" ); } catch (PluginLoadException ex) { @@ -445,19 +455,31 @@ public PluginHandle getPlugin(String pluginId) { */ public PluginHandle getPluginWithRequestCode(int requestCode) { for (PluginHandle handle : this.plugins.values()) { - NativePlugin pluginAnnotation = handle.getPluginAnnotation(); + int[] requestCodes; + int permissionRequestCode; + + CapacitorPlugin pluginAnnotation = handle.getPluginAnnotation(); if (pluginAnnotation == null) { - continue; + // Check for legacy plugin annotation, @NativePlugin + NativePlugin legacyPluginAnnotation = handle.getLegacyPluginAnnotation(); + if (legacyPluginAnnotation == null) { + continue; + } + + requestCodes = legacyPluginAnnotation.requestCodes(); + permissionRequestCode = legacyPluginAnnotation.permissionRequestCode(); + } else { + requestCodes = pluginAnnotation.requestCodes(); + permissionRequestCode = pluginAnnotation.permissionRequestCode(); } - int[] requestCodes = pluginAnnotation.requestCodes(); for (int rc : requestCodes) { if (rc == requestCode) { return handle; } } - if (pluginAnnotation.permissionRequestCode() == requestCode) { + if (permissionRequestCode == requestCode) { return handle; } } @@ -677,7 +699,6 @@ public void startActivityForPluginWithResult(PluginCall call, Intent intent, int * @param permissions the permissions requested * @param grantResults the set of granted/denied permissions */ - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { PluginHandle plugin = getPluginWithRequestCode(requestCode); @@ -691,7 +712,12 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in return; } - plugin.getInstance().handleRequestPermissionsResult(requestCode, permissions, grantResults); + if (plugin.getPluginAnnotation() != null) { + plugin.getInstance().onRequestPermissionsResult(requestCode, permissions, grantResults); + } else { + // Call deprecated method if using deprecated NativePlugin annotation + plugin.getInstance().handleRequestPermissionsResult(requestCode, permissions, grantResults); + } } /** diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java index 313b37609a..f259372118 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java +++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java @@ -5,7 +5,6 @@ import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.Intent; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Environment; @@ -21,8 +20,8 @@ import android.webkit.WebChromeClient; import android.webkit.WebView; import android.widget.EditText; -import androidx.core.app.ActivityCompat; import androidx.core.content.FileProvider; +import com.getcapacitor.util.PermissionHelper; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; @@ -45,7 +44,6 @@ public class BridgeWebChromeClient extends WebChromeClient { static final int FILE_CHOOSER_CAMERA_PERMISSION = PluginRequestCodes.FILE_CHOOSER_CAMERA_PERMISSION; static final int GET_USER_MEDIA_PERMISSIONS = PluginRequestCodes.GET_USER_MEDIA_PERMISSIONS; static final int GEOLOCATION_REQUEST_PERMISSIONS = PluginRequestCodes.GEOLOCATION_REQUEST_PERMISSIONS; - static final String[] geoPermissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }; private Bridge bridge; @@ -262,7 +260,9 @@ public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermiss super.onGeolocationPermissionsShowPrompt(origin, callback); Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: " + origin); - if (!hasPermissions(geoPermissions)) { + final String[] geoPermissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }; + + if (!PermissionHelper.hasPermissions(bridge.getContext(), geoPermissions)) { this.bridge.cordovaInterface.requestPermissions( new CordovaPlugin() { @Override @@ -333,7 +333,10 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, int private boolean isMediaCaptureSupported() { String[] permissions = { Manifest.permission.CAMERA }; - return hasPermissions(permissions) || !hasDefinedPermission(Manifest.permission.CAMERA); + return ( + PermissionHelper.hasPermissions(bridge.getContext(), permissions) || + !PermissionHelper.hasDefinedPermission(bridge.getContext(), Manifest.permission.CAMERA) + ); } private void showMediaCaptureOrFilePicker(ValueCallback filePathCallback, FileChooserParams fileChooserParams, boolean isVideo) { @@ -513,39 +516,4 @@ private File createImageFile(Activity activity) throws IOException { return image; } - - private boolean hasPermissions(String[] permissions) { - for (String perm : permissions) { - if (ActivityCompat.checkSelfPermission(this.bridge.getActivity(), perm) != PackageManager.PERMISSION_GRANTED) { - return false; - } - } - return true; - } - - private boolean hasDefinedPermission(String permission) { - boolean hasPermission = false; - String[] requestedPermissions = getManifestPermissions(); - if (requestedPermissions != null && requestedPermissions.length > 0) { - List requestedPermissionsList = Arrays.asList(requestedPermissions); - ArrayList requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList); - if (requestedPermissionsArrayList.contains(permission)) { - hasPermission = true; - } - } - return hasPermission; - } - - private String[] getManifestPermissions() { - String[] requestedPermissions = null; - try { - PackageManager pm = bridge.getContext().getPackageManager(); - PackageInfo packageInfo = pm.getPackageInfo(bridge.getContext().getPackageName(), PackageManager.GET_PERMISSIONS); - - if (packageInfo != null) { - requestedPermissions = packageInfo.requestedPermissions; - } - } catch (Exception ex) {} - return requestedPermissions; - } } diff --git a/android/capacitor/src/main/java/com/getcapacitor/NativePlugin.java b/android/capacitor/src/main/java/com/getcapacitor/NativePlugin.java index 801661e4b3..f47c13fbc2 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/NativePlugin.java +++ b/android/capacitor/src/main/java/com/getcapacitor/NativePlugin.java @@ -1,12 +1,16 @@ package com.getcapacitor; +import com.getcapacitor.annotation.CapacitorPlugin; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Base annotation for all Plugins + * @deprecated + *

Use {@link CapacitorPlugin} instead */ @Retention(RetentionPolicy.RUNTIME) +@Deprecated public @interface NativePlugin { /** * Request codes this plugin uses and responds to, in order to tie diff --git a/android/capacitor/src/main/java/com/getcapacitor/Plugin.java b/android/capacitor/src/main/java/com/getcapacitor/Plugin.java index cc50c7c362..28307f6efb 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/Plugin.java +++ b/android/capacitor/src/main/java/com/getcapacitor/Plugin.java @@ -3,17 +3,21 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageInfo; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; +import com.getcapacitor.annotation.CapacitorPlugin; +import com.getcapacitor.annotation.Permission; +import com.getcapacitor.util.PermissionHelper; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.json.JSONException; import org.json.JSONObject; @@ -32,6 +36,9 @@ public class Plugin { // for a plugin call options. private static final String BUNDLE_PERSISTED_OPTIONS_JSON_KEY = "_json"; + // The name of the Shared Preferences file for permission use + private static final String PERMISSION_PREFS = "PluginPermStates"; + // Reference to the Bridge protected Bridge bridge; @@ -161,62 +168,17 @@ public Object getConfigValue(String key) { } /** - * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml - * @param neededPermissions - * @return - */ - public String[] getUndefinedPermissions(String[] neededPermissions) { - ArrayList undefinedPermissions = new ArrayList<>(); - String[] requestedPermissions = getManifestPermissions(); - if (requestedPermissions != null && requestedPermissions.length > 0) { - List requestedPermissionsList = Arrays.asList(requestedPermissions); - ArrayList requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList); - for (String permission : neededPermissions) { - if (!requestedPermissionsArrayList.contains(permission)) { - undefinedPermissions.add(permission); - } - } - String[] undefinedPermissionArray = new String[undefinedPermissions.size()]; - undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray); - - return undefinedPermissionArray; - } - return neededPermissions; - } - - /** - * Check whether the given permission has been defined in the AndroidManifest.xml - * @param permission + * Check whether any of the given permissions has been defined in the AndroidManifest.xml + * @param permissions * @return */ - public boolean hasDefinedPermission(String permission) { - boolean hasPermission = false; - String[] requestedPermissions = getManifestPermissions(); - if (requestedPermissions != null && requestedPermissions.length > 0) { - List requestedPermissionsList = Arrays.asList(requestedPermissions); - ArrayList requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList); - if (requestedPermissionsArrayList.contains(permission)) { - hasPermission = true; + public boolean hasDefinedPermissions(String[] permissions) { + for (String permission : permissions) { + if (!PermissionHelper.hasDefinedPermission(getContext(), permission)) { + return false; } } - return hasPermission; - } - - /** - * Get the permissions defined in AndroidManifest.xml - * @return - */ - private String[] getManifestPermissions() { - String[] requestedPermissions = null; - try { - PackageManager pm = getContext().getPackageManager(); - PackageInfo packageInfo = pm.getPackageInfo(getAppId(), PackageManager.GET_PERMISSIONS); - - if (packageInfo != null) { - requestedPermissions = packageInfo.requestedPermissions; - } - } catch (Exception ex) {} - return requestedPermissions; + return true; } /** @@ -224,9 +186,9 @@ private String[] getManifestPermissions() { * @param permissions * @return */ - public boolean hasDefinedPermissions(String[] permissions) { - for (String permission : permissions) { - if (!hasDefinedPermission(permission)) { + public boolean hasDefinedPermissions(Permission[] permissions) { + for (Permission perm : permissions) { + if (!PermissionHelper.hasDefinedPermission(getContext(), perm.permission())) { return false; } } @@ -238,7 +200,13 @@ public boolean hasDefinedPermissions(String[] permissions) { * @return */ public boolean hasDefinedRequiredPermissions() { - NativePlugin annotation = handle.getPluginAnnotation(); + CapacitorPlugin annotation = handle.getPluginAnnotation(); + if (annotation == null) { + // Check for legacy plugin annotation, @NativePlugin + NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation(); + return hasDefinedPermissions(legacyAnnotation.permissions()); + } + return hasDefinedPermissions(annotation.permissions()); } @@ -259,15 +227,75 @@ public boolean hasPermission(String permission) { * @return */ public boolean hasRequiredPermissions() { - NativePlugin annotation = handle.getPluginAnnotation(); - for (String perm : annotation.permissions()) { - if (!hasPermission(perm)) { + CapacitorPlugin annotation = handle.getPluginAnnotation(); + if (annotation == null) { + // Check for legacy plugin annotation, @NativePlugin + NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation(); + for (String perm : legacyAnnotation.permissions()) { + if (!hasPermission(perm)) { + return false; + } + } + + return true; + } + + for (Permission perm : annotation.permissions()) { + if (!hasPermission(perm.permission())) { return false; } } + return true; } + /** + * Exported plugin call for checking the granted status for each permission + * declared on the plugin. This plugin call responds with a mapping of permissions to + * the associated granted status. + * + * @since 3.0.0 + */ + @PluginMethod + public void checkPermissions(PluginCall pluginCall) { + JSObject permissionsResult = getPermissionStates(); + pluginCall.resolve(permissionsResult); + } + + /** + * 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. + */ + public JSObject getPermissionStates() { + JSObject permissionsResults = new JSObject(); + CapacitorPlugin annotation = handle.getPluginAnnotation(); + for (Permission perm : annotation.permissions()) { + String key = perm.alias().isEmpty() ? perm.permission() : perm.alias(); + String permissionStatus = hasPermission(perm.permission()) ? "granted" : "prompt"; + + // Check if there is a cached permission state for the "Never ask again" state + if (permissionStatus.equals("prompt")) { + SharedPreferences prefs = getContext().getSharedPreferences(PERMISSION_PREFS, Activity.MODE_PRIVATE); + String state = prefs.getString(perm.permission(), null); + + if (state != null) { + permissionStatus = state; + } + } + + String existingResult = permissionsResults.getString(key); + + // multiple permissions with the same alias must all be true, otherwise all false. + if (existingResult == null || existingResult.equals("granted")) { + permissionsResults.put(key, permissionStatus); + } + } + + return permissionsResults; + } + /** * Helper to make requesting permissions easy * @param permissions the set of permissions to request @@ -278,11 +306,22 @@ public void pluginRequestPermissions(String[] permissions, int requestCode) { } /** - * Request all of the specified permissions in the NativePlugin annotation (if any) + * Request all of the specified permissions in the CapacitorPlugin annotation (if any) */ public void pluginRequestAllPermissions() { - NativePlugin annotation = handle.getPluginAnnotation(); - ActivityCompat.requestPermissions(getActivity(), annotation.permissions(), annotation.permissionRequestCode()); + CapacitorPlugin annotation = handle.getPluginAnnotation(); + if (annotation == null) { + NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation(); + ActivityCompat.requestPermissions(getActivity(), legacyAnnotation.permissions(), legacyAnnotation.permissionRequestCode()); + return; + } + + String[] perms = new String[annotation.permissions().length]; + for (int i = 0; i < perms.length; i++) { + perms[i] = annotation.permissions()[i].permission(); + } + + ActivityCompat.requestPermissions(getActivity(), perms, annotation.permissionRequestCode()); } /** @@ -428,36 +467,76 @@ public void removeAllListeners(PluginCall call) { * Exported plugin call to request all permissions for this plugin * @param call */ - @SuppressWarnings("unused") @PluginMethod public void requestPermissions(PluginCall call) { - // Should be overridden, does nothing by default - NativePlugin annotation = this.handle.getPluginAnnotation(); - String[] perms = annotation.permissions(); + String[] perms = null; + int permissionRequestCode; + + // If call was made with a list of permissions to request, save them to be requested + // instead of all permissions + JSArray providedPerms = call.getArray("permissions"); + List providedPermsList = null; - if (perms.length > 0) { + try { + providedPermsList = providedPerms.toList(); + } catch (JSONException e) {} + + CapacitorPlugin annotation = handle.getPluginAnnotation(); + if (annotation == null) { + NativePlugin legacyAnnotation = this.handle.getLegacyPluginAnnotation(); + perms = legacyAnnotation.permissions(); + permissionRequestCode = legacyAnnotation.permissionRequestCode(); + } else { + // If call was made without any custom permissions, request all from plugin annotation + if (providedPermsList == null || providedPermsList.isEmpty()) { + perms = new String[annotation.permissions().length]; + for (int i = 0; i < perms.length; i++) { + perms[i] = annotation.permissions()[i].permission(); + } + } else { + Set permsSet = new HashSet<>(); + for (Permission perm : annotation.permissions()) { + if (providedPermsList.contains(perm.alias()) || providedPermsList.contains(perm.permission())) { + permsSet.add(perm.permission()); + } + } + + if (permsSet.isEmpty()) { + call.reject("No valid permission or permission alias was requested."); + } else { + perms = permsSet.toArray(new String[0]); + } + } + + permissionRequestCode = annotation.permissionRequestCode(); + } + + if (perms != null && perms.length > 0) { // Save the call so we can return data back once the permission request has completed saveCall(call); - pluginRequestPermissions(perms, annotation.permissionRequestCode()); + pluginRequestPermissions(perms, permissionRequestCode); } else { call.resolve(); } } /** - * Handle request permissions result. A plugin can override this to handle the result - * themselves, or this method will handle the result for our convenient requestPermissions - * call. + * Handle request permissions result. A plugin using the deprecated {@link NativePlugin} + * should override this to handle the result, or this method will handle the result + * for our convenient requestPermissions call. + * @deprecated in favor of using {@link #onRequestPermissionsResult} in conjunction with {@link CapacitorPlugin} + * * @param requestCode * @param permissions * @param grantResults */ + @Deprecated protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (!hasDefinedPermissions(permissions)) { StringBuilder builder = new StringBuilder(); builder.append("Missing the following permissions in AndroidManifest.xml:\n"); - String[] missing = getUndefinedPermissions(permissions); + String[] missing = PermissionHelper.getUndefinedPermissions(getContext(), permissions); for (String perm : missing) { builder.append(perm + "\n"); } @@ -466,6 +545,54 @@ protected void handleRequestPermissionsResult(int requestCode, String[] permissi } } + /** + * Handle request permissions result. A plugin using the {@link CapacitorPlugin} annotation + * can override this to handle the result, or this method will handle the result for + * our convenient requestPermissions call. + * + * @param requestCode + * @param permissions + * @param grantResults + */ + protected void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + PluginCall savedCall = getSavedCall(); + if (savedCall == null) { + return; + } + + SharedPreferences prefs = getContext().getSharedPreferences(PERMISSION_PREFS, 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); + + if (state != null) { + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(permission); + editor.apply(); + } + } + } else { + for (String permission : permissions) { + SharedPreferences.Editor editor = prefs.edit(); + + if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permission)) { + // Permission denied, can prompt again with rationale + editor.putString(permission, "prompt-with-rationale"); + } else { + // Permission denied permanently, store this state for future reference + editor.putString(permission, "denied"); + } + + editor.apply(); + } + } + + savedCall.resolve(getPermissionStates()); + savedCall.release(bridge); + } + /** * Called before the app is destroyed to give a plugin the chance to * save the last call options for a saved plugin. By default, this diff --git a/android/capacitor/src/main/java/com/getcapacitor/PluginHandle.java b/android/capacitor/src/main/java/com/getcapacitor/PluginHandle.java index 17e61084a2..eeedd68852 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/PluginHandle.java +++ b/android/capacitor/src/main/java/com/getcapacitor/PluginHandle.java @@ -1,5 +1,6 @@ package com.getcapacitor; +import com.getcapacitor.annotation.CapacitorPlugin; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; @@ -19,25 +20,39 @@ public class PluginHandle { private final String pluginId; - private NativePlugin pluginAnnotation; + private NativePlugin legacyPluginAnnotation; + private CapacitorPlugin pluginAnnotation; + private Plugin instance; public PluginHandle(Bridge bridge, Class pluginClass) throws InvalidPluginException, PluginLoadException { this.bridge = bridge; this.pluginClass = pluginClass; - NativePlugin pluginAnnotation = pluginClass.getAnnotation(NativePlugin.class); + CapacitorPlugin pluginAnnotation = pluginClass.getAnnotation(CapacitorPlugin.class); if (pluginAnnotation == null) { - throw new InvalidPluginException("No @NativePlugin annotation found for plugin " + pluginClass.getName()); - } + // Check for legacy plugin annotation, @NativePlugin + NativePlugin legacyPluginAnnotation = pluginClass.getAnnotation(NativePlugin.class); + if (legacyPluginAnnotation == null) { + throw new InvalidPluginException("No @CapacitorPlugin annotation found for plugin " + pluginClass.getName()); + } + + if (!legacyPluginAnnotation.name().equals("")) { + this.pluginId = legacyPluginAnnotation.name(); + } else { + this.pluginId = pluginClass.getSimpleName(); + } - if (!pluginAnnotation.name().equals("")) { - this.pluginId = pluginAnnotation.name(); + this.legacyPluginAnnotation = legacyPluginAnnotation; } else { - this.pluginId = pluginClass.getSimpleName(); - } + if (!pluginAnnotation.name().equals("")) { + this.pluginId = pluginAnnotation.name(); + } else { + this.pluginId = pluginClass.getSimpleName(); + } - this.pluginAnnotation = pluginAnnotation; + this.pluginAnnotation = pluginAnnotation; + } this.indexMethods(pluginClass); @@ -52,7 +67,11 @@ public String getId() { return this.pluginId; } - public NativePlugin getPluginAnnotation() { + public NativePlugin getLegacyPluginAnnotation() { + return this.legacyPluginAnnotation; + } + + public CapacitorPlugin getPluginAnnotation() { return this.pluginAnnotation; } diff --git a/android/capacitor/src/main/java/com/getcapacitor/annotation/CapacitorPlugin.java b/android/capacitor/src/main/java/com/getcapacitor/annotation/CapacitorPlugin.java new file mode 100644 index 0000000000..9234403bfd --- /dev/null +++ b/android/capacitor/src/main/java/com/getcapacitor/annotation/CapacitorPlugin.java @@ -0,0 +1,33 @@ +package com.getcapacitor.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Base annotation for all Plugins + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface CapacitorPlugin { + /** + * Request codes this plugin uses and responds to, in order to tie + * Android events back the plugin to handle + */ + int[] requestCodes() default {}; + + /** + * Permissions this plugin needs, in order to make permission requests + * easy if the plugin only needs basic permission prompting + */ + Permission[] permissions() default {}; + + /** + * The request code to use when automatically requesting permissions + */ + int permissionRequestCode() default 9000; + + /** + * A custom name for the plugin, otherwise uses the + * simple class name. + */ + String name() default ""; +} diff --git a/android/capacitor/src/main/java/com/getcapacitor/annotation/Permission.java b/android/capacitor/src/main/java/com/getcapacitor/annotation/Permission.java new file mode 100644 index 0000000000..d87e18c326 --- /dev/null +++ b/android/capacitor/src/main/java/com/getcapacitor/annotation/Permission.java @@ -0,0 +1,22 @@ +package com.getcapacitor.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Permission annotation for use with @CapacitorPlugin + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Permission { + /** + * The Android string for the permission. + * Eg: Manifest.permission.ACCESS_COARSE_LOCATION + * or "android.permission.ACCESS_COARSE_LOCATION" + */ + String permission() default ""; + + /** + * An optional name to use instead of the Android permission string. + */ + String alias() default ""; +} diff --git a/android/capacitor/src/main/java/com/getcapacitor/util/PermissionHelper.java b/android/capacitor/src/main/java/com/getcapacitor/util/PermissionHelper.java new file mode 100644 index 0000000000..057f32faa7 --- /dev/null +++ b/android/capacitor/src/main/java/com/getcapacitor/util/PermissionHelper.java @@ -0,0 +1,98 @@ +package com.getcapacitor.util; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import androidx.core.app.ActivityCompat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A helper class for checking permissions. + * + * @since 3.0.0 + */ +public class PermissionHelper { + + /** + * Checks if a list of given permissions are all granted by the user + * + * @since 3.0.0 + * @param permissions Permissions to check. + * @return True if all permissions are granted, false if at least one is not. + */ + public static boolean hasPermissions(Context context, String[] permissions) { + for (String perm : permissions) { + if (ActivityCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + return true; + } + + /** + * Check whether the given permission has been defined in the AndroidManifest.xml + * + * @since 3.0.0 + * @param permission A permission to check. + * @return True if the permission has been defined in the Manifest, false if not. + */ + public static boolean hasDefinedPermission(Context context, String permission) { + boolean hasPermission = false; + String[] requestedPermissions = PermissionHelper.getManifestPermissions(context); + if (requestedPermissions != null && requestedPermissions.length > 0) { + List requestedPermissionsList = Arrays.asList(requestedPermissions); + ArrayList requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList); + if (requestedPermissionsArrayList.contains(permission)) { + hasPermission = true; + } + } + return hasPermission; + } + + /** + * Get the permissions defined in AndroidManifest.xml + * + * @since 3.0.0 + * @return The permissions defined in AndroidManifest.xml + */ + public static String[] getManifestPermissions(Context context) { + String[] requestedPermissions = null; + try { + PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); + + if (packageInfo != null) { + requestedPermissions = packageInfo.requestedPermissions; + } + } catch (Exception ex) {} + return requestedPermissions; + } + + /** + * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml + * + * @since 3.0.0 + * @param neededPermissions The permissions needed. + * @return The permissions not present in AndroidManifest.xml + */ + public static String[] getUndefinedPermissions(Context context, String[] neededPermissions) { + ArrayList undefinedPermissions = new ArrayList<>(); + String[] requestedPermissions = getManifestPermissions(context); + if (requestedPermissions != null && requestedPermissions.length > 0) { + List requestedPermissionsList = Arrays.asList(requestedPermissions); + ArrayList requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList); + for (String permission : neededPermissions) { + if (!requestedPermissionsArrayList.contains(permission)) { + undefinedPermissions.add(permission); + } + } + String[] undefinedPermissionArray = new String[undefinedPermissions.size()]; + undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray); + + return undefinedPermissionArray; + } + return neededPermissions; + } +} diff --git a/core/src/definitions.ts b/core/src/definitions.ts index fb5cd9bc14..0999d51e47 100644 --- a/core/src/definitions.ts +++ b/core/src/definitions.ts @@ -5,12 +5,13 @@ export interface Plugin { eventName: string, listenerFunc: (...args: any[]) => any, ): PluginListenerHandle; - requestPermissions?: () => Promise; } -export interface PermissionsRequestResult { - results: any[]; -} +export type PermissionState = + | 'prompt' + | 'prompt-with-rationale' + | 'granted' + | 'denied'; export interface PluginListenerHandle { remove: () => void; diff --git a/core/src/web/index.ts b/core/src/web/index.ts index f8d1e2804d..2d1b8f571d 100644 --- a/core/src/web/index.ts +++ b/core/src/web/index.ts @@ -1,8 +1,4 @@ -import type { - CapacitorException, - PluginListenerHandle, - PermissionsRequestResult, -} from '../definitions'; +import type { CapacitorException, PluginListenerHandle } from '../definitions'; import { ExceptionCode } from '../definitions'; import { Capacitor } from '../global'; @@ -136,18 +132,6 @@ export class WebPlugin { }; } - requestPermissions(): Promise { - if (Capacitor.isNative) { - return Capacitor.nativePromise( - this.config.name, - 'requestPermissions', - {}, - ); - } else { - return Promise.resolve({ results: [] }); - } - } - load(): void { this.loaded = true; }