package com.getcapacitor;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import com.getcapacitor.annotation.ActivityCallback;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.annotation.Permission;
import com.getcapacitor.annotation.PermissionCallback;
import com.getcapacitor.util.PermissionHelper;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.json.JSONException;

/**
 * Plugin is the base class for all plugins, containing a number of
 * convenient features for interacting with the {@link Bridge}, managing
 * plugin permissions, tracking lifecycle events, and more.
 *
 * You should inherit from this class when creating new plugins, along with
 * adding the {@link CapacitorPlugin} annotation to add additional required
 * metadata about the Plugin
 */
public class Plugin {

    // The key we will use inside of a persisted Bundle for the JSON blob
    // for a plugin call options.
    private static final String BUNDLE_PERSISTED_OPTIONS_JSON_KEY = "_json";

    // Reference to the Bridge
    protected Bridge bridge;

    // Reference to the PluginHandle wrapper for this Plugin
    protected PluginHandle handle;

    /**
     * A way for plugins to quickly save a call that they will need to reference
     * between activity/permissions starts/requests
     *
     * @deprecated store calls on the bridge using the methods
     * {@link com.getcapacitor.Bridge#saveCall(PluginCall)},
     * {@link com.getcapacitor.Bridge#getSavedCall(String)} and
     * {@link com.getcapacitor.Bridge#releaseCall(PluginCall)}
     */
    @Deprecated
    protected PluginCall savedLastCall;

    // Stored event listeners
    private final Map<String, List<PluginCall>> eventListeners;

    /**
     * Launchers used by the plugin to handle activity results
     */
    private final Map<String, ActivityResultLauncher<Intent>> activityLaunchers = new HashMap<>();

    /**
     * Launchers used by the plugin to handle permission results
     */
    private final Map<String, ActivityResultLauncher<String[]>> permissionLaunchers = new HashMap<>();

    private String lastPluginCallId;

    // Stored results of an event if an event was fired and
    // no listeners were attached yet. Only stores the last value.
    private final Map<String, List<JSObject>> retainedEventArguments;

    public Plugin() {
        eventListeners = new HashMap<>();
        retainedEventArguments = new HashMap<>();
    }

    /**
     * Called when the plugin has been connected to the bridge
     * and is ready to start initializing.
     */
    public void load() {}

    /**
     * Registers activity result launchers defined on plugins, used for permission requests and
     * activities started for result.
     */
    void initializeActivityLaunchers() {
        List<Method> pluginClassMethods = new ArrayList<>();
        for (
            Class<?> pluginCursor = getClass();
            !pluginCursor.getName().equals(Object.class.getName());
            pluginCursor = pluginCursor.getSuperclass()
        ) {
            pluginClassMethods.addAll(Arrays.asList(pluginCursor.getDeclaredMethods()));
        }

        for (final Method method : pluginClassMethods) {
            if (method.isAnnotationPresent(ActivityCallback.class)) {
                // register callbacks annotated with ActivityCallback for activity results
                ActivityResultLauncher<Intent> launcher = bridge.registerForActivityResult(
                    new ActivityResultContracts.StartActivityForResult(),
                    result -> triggerActivityCallback(method, result)
                );

                activityLaunchers.put(method.getName(), launcher);
            } else if (method.isAnnotationPresent(PermissionCallback.class)) {
                // register callbacks annotated with PermissionCallback for permission results
                ActivityResultLauncher<String[]> launcher = bridge.registerForActivityResult(
                    new ActivityResultContracts.RequestMultiplePermissions(),
                    permissions -> triggerPermissionCallback(method, permissions)
                );

                permissionLaunchers.put(method.getName(), launcher);
            }
        }
    }

    private void triggerPermissionCallback(Method method, Map<String, Boolean> permissionResultMap) {
        PluginCall savedCall = bridge.getPermissionCall(handle.getId());

        // validate permissions and invoke the permission result callback
        if (bridge.validatePermissions(this, savedCall, permissionResultMap)) {
            try {
                method.setAccessible(true);
                method.invoke(this, savedCall);
            } catch (IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

    private void triggerActivityCallback(Method method, ActivityResult result) {
        PluginCall savedCall = bridge.getSavedCall(lastPluginCallId);
        if (savedCall == null) {
            savedCall = bridge.getPluginCallForLastActivity();
        }
        // invoke the activity result callback
        try {
            method.setAccessible(true);
            method.invoke(this, savedCall, result);
        } catch (IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    /**
     * Start activity for result with the provided Intent and resolve with the provided callback method name.
     * <p>
     * If there is no registered activity callback for the method name passed in, the call will
     * be rejected. Make sure a valid activity result callback method is registered using the
     * {@link ActivityCallback} annotation.
     *
     * @param call the plugin call
     * @param intent the intent used to start an activity
     * @param callbackName the name of the callback to run when the launched activity is finished
     * @since 3.0.0
     */
    public void startActivityForResult(PluginCall call, Intent intent, String callbackName) {
        ActivityResultLauncher<Intent> activityResultLauncher = getActivityLauncherOrReject(call, callbackName);
        if (activityResultLauncher == null) {
            // return when null since call was rejected in getLauncherOrReject
            return;
        }
        bridge.setPluginCallForLastActivity(call);
        lastPluginCallId = call.getCallbackId();
        bridge.saveCall(call);
        activityResultLauncher.launch(intent);
    }

    private void permissionActivityResult(PluginCall call, String[] permissionStrings, String callbackName) {
        ActivityResultLauncher<String[]> permissionResultLauncher = getPermissionLauncherOrReject(call, callbackName);
        if (permissionResultLauncher == null) {
            // return when null since call was rejected in getLauncherOrReject
            return;
        }

        bridge.savePermissionCall(call);
        permissionResultLauncher.launch(permissionStrings);
    }

    /**
     * Get the main {@link Context} for the current Activity (your app)
     * @return the Context for the current activity
     */
    public Context getContext() {
        return this.bridge.getContext();
    }

    /**
     * Get the main {@link Activity} for the app
     * @return the Activity for the current app
     */
    public AppCompatActivity getActivity() {
        return this.bridge.getActivity();
    }

    /**
     * Set the Bridge instance for this plugin
     * @param bridge
     */
    public void setBridge(Bridge bridge) {
        this.bridge = bridge;
    }

    /**
     * Get the Bridge instance for this plugin
     */
    public Bridge getBridge() {
        return this.bridge;
    }

    /**
     * Set the wrapper {@link PluginHandle} instance for this plugin that
     * contains additional metadata about the Plugin instance (such
     * as indexed methods for reflection, and {@link CapacitorPlugin} annotation data).
     * @param pluginHandle
     */
    public void setPluginHandle(PluginHandle pluginHandle) {
        this.handle = pluginHandle;
    }

    /**
     * Return the wrapper {@link PluginHandle} for this plugin.
     *
     * This wrapper contains additional metadata about the plugin instance,
     * such as indexed methods for reflection, and {@link CapacitorPlugin} annotation data).
     * @return
     */
    public PluginHandle getPluginHandle() {
        return this.handle;
    }

    /**
     * Get the root App ID
     * @return
     */
    public String getAppId() {
        return getContext().getPackageName();
    }

    /**
     * Called to save a {@link PluginCall} in order to reference it
     * later, such as in an activity or permissions result handler
     * @deprecated use {@link Bridge#saveCall(PluginCall)}
     *
     * @param lastCall
     */
    @Deprecated
    public void saveCall(PluginCall lastCall) {
        this.savedLastCall = lastCall;
    }

    /**
     * Set the last saved call to null to free memory
     * @deprecated use {@link PluginCall#release(Bridge)}
     */
    @Deprecated
    public void freeSavedCall() {
        this.savedLastCall.release(bridge);
        this.savedLastCall = null;
    }

    /**
     * Get the last saved call, if any
     * @deprecated use {@link Bridge#getSavedCall(String)}
     *
     * @return
     */
    @Deprecated
    public PluginCall getSavedCall() {
        return this.savedLastCall;
    }

    /**
     * Get the config options for this plugin.
     *
     * @return a config object representing the plugin config options, or an empty config
     * if none exists
     */
    public PluginConfig getConfig() {
        return bridge.getConfig().getPluginConfiguration(handle.getId());
    }

    /**
     * Get the value for a key on the config for this plugin.
     * @deprecated use {@link #getConfig()} and access config values using the methods available
     * depending on the type.
     *
     * @param key the key for the config value
     * @return some object containing the value from the config
     */
    @Deprecated
    public Object getConfigValue(String key) {
        try {
            PluginConfig pluginConfig = getConfig();
            return pluginConfig.getConfigJSON().get(key);
        } catch (JSONException ex) {
            return null;
        }
    }

    /**
     * Check whether any of the given permissions has been defined in the AndroidManifest.xml
     * @deprecated use {@link #isPermissionDeclared(String)}
     *
     * @param permissions
     * @return
     */
    @Deprecated
    public boolean hasDefinedPermissions(String[] permissions) {
        for (String permission : permissions) {
            if (!PermissionHelper.hasDefinedPermission(getContext(), permission)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Check if all annotated permissions have been defined in the AndroidManifest.xml
     * @deprecated use {@link #isPermissionDeclared(String)}
     *
     * @return true if permissions are all defined in the Manifest
     */
    @Deprecated
    public boolean hasDefinedRequiredPermissions() {
        CapacitorPlugin annotation = handle.getPluginAnnotation();
        if (annotation == null) {
            // Check for legacy plugin annotation, @NativePlugin
            NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation();
            return hasDefinedPermissions(legacyAnnotation.permissions());
        } else {
            for (Permission perm : annotation.permissions()) {
                for (String permString : perm.strings()) {
                    if (!PermissionHelper.hasDefinedPermission(getContext(), permString)) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Checks if the given permission alias is correctly declared in AndroidManifest.xml
     * @param alias a permission alias defined on the plugin
     * @return true only if all permissions associated with the given alias are declared in the manifest
     */
    public boolean isPermissionDeclared(String alias) {
        CapacitorPlugin annotation = handle.getPluginAnnotation();
        if (annotation != null) {
            for (Permission perm : annotation.permissions()) {
                if (alias.equalsIgnoreCase(perm.alias())) {
                    boolean result = true;
                    for (String permString : perm.strings()) {
                        result = result && PermissionHelper.hasDefinedPermission(getContext(), permString);
                    }

                    return result;
                }
            }
        }

        Logger.error(String.format("isPermissionDeclared: No alias defined for %s " + "or missing @CapacitorPlugin annotation.", alias));
        return false;
    }

    /**
     * Check whether the given permission has been granted by the user
     * @deprecated use {@link #getPermissionState(String)} and {@link #getPermissionStates()} to get
     * the states of permissions defined on the Plugin in conjunction with the @CapacitorPlugin
     * annotation. Use the Android API {@link ActivityCompat#checkSelfPermission(Context, String)}
     * methods to check permissions with Android permission strings
     *
     * @param permission
     * @return
     */
    @Deprecated
    public boolean hasPermission(String permission) {
        return ActivityCompat.checkSelfPermission(this.getContext(), permission) == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * If the plugin annotation specified a set of permissions, this method checks if each is
     * granted
     * @deprecated use {@link #getPermissionState(String)} or {@link #getPermissionStates()} to
     * check whether permissions are granted or not
     *
     * @return
     */
    @Deprecated
    public boolean hasRequiredPermissions() {
        CapacitorPlugin annotation = handle.getPluginAnnotation();
        if (annotation == null) {
            // Check for legacy plugin annotation, @NativePlugin
            NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation();
            for (String perm : legacyAnnotation.permissions()) {
                if (ActivityCompat.checkSelfPermission(this.getContext(), perm) != PackageManager.PERMISSION_GRANTED) {
                    return false;
                }
            }

            return true;
        }

        for (Permission perm : annotation.permissions()) {
            for (String permString : perm.strings()) {
                if (ActivityCompat.checkSelfPermission(this.getContext(), permString) != PackageManager.PERMISSION_GRANTED) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Request all of the specified permissions in the CapacitorPlugin annotation (if any)
     *
     * If there is no registered permission callback for the PluginCall passed in, the call will
     * be rejected. Make sure a valid permission callback method is registered using the
     * {@link PermissionCallback} annotation.
     *
     * @since 3.0.0
     * @param call the plugin call
     * @param callbackName the name of the callback to run when the permission request is complete
     */
    protected void requestAllPermissions(@NonNull PluginCall call, @NonNull String callbackName) {
        CapacitorPlugin annotation = handle.getPluginAnnotation();
        if (annotation != null) {
            HashSet<String> perms = new HashSet<>();
            for (Permission perm : annotation.permissions()) {
                perms.addAll(Arrays.asList(perm.strings()));
            }

            permissionActivityResult(call, perms.toArray(new String[0]), callbackName);
        }
    }

    /**
     * Request permissions using an alias defined on the plugin.
     *
     * If there is no registered permission callback for the PluginCall passed in, the call will
     * be rejected. Make sure a valid permission callback method is registered using the
     * {@link PermissionCallback} annotation.
     *
     * @param alias an alias defined on the plugin
     * @param call  the plugin call involved in originating the request
     * @param callbackName the name of the callback to run when the permission request is complete
     */
    protected void requestPermissionForAlias(@NonNull String alias, @NonNull PluginCall call, @NonNull String callbackName) {
        requestPermissionForAliases(new String[] { alias }, call, callbackName);
    }

    /**
     * Request permissions using aliases defined on the plugin.
     *
     * If there is no registered permission callback for the PluginCall passed in, the call will
     * be rejected. Make sure a valid permission callback method is registered using the
     * {@link PermissionCallback} annotation.
     *
     * @param aliases a set of aliases defined on the plugin
     * @param call    the plugin call involved in originating the request
     * @param callbackName the name of the callback to run when the permission request is complete
     */
    protected void requestPermissionForAliases(@NonNull String[] aliases, @NonNull PluginCall call, @NonNull String callbackName) {
        if (aliases.length == 0) {
            Logger.error("No permission alias was provided");
            return;
        }

        String[] permissions = getPermissionStringsForAliases(aliases);

        if (permissions.length > 0) {
            permissionActivityResult(call, permissions, callbackName);
        }
    }

    /**
     * Gets the Android permission strings defined on the {@link CapacitorPlugin} annotation with
     * the provided aliases.
     *
     * @param aliases aliases for permissions defined on the plugin
     * @return Android permission strings associated with the provided aliases, if exists
     */
    private String[] getPermissionStringsForAliases(@NonNull String[] aliases) {
        CapacitorPlugin annotation = handle.getPluginAnnotation();
        HashSet<String> perms = new HashSet<>();
        for (Permission perm : annotation.permissions()) {
            if (Arrays.asList(aliases).contains(perm.alias())) {
                perms.addAll(Arrays.asList(perm.strings()));
            }
        }

        return perms.toArray(new String[0]);
    }

    /**
     * Gets the activity launcher associated with the calling methodName, or rejects the call if
     * no registered launcher exists
     *
     * @param call       the plugin call
     * @param methodName the name of the activity callback method
     * @return a launcher, or null if none found
     */
    private @Nullable ActivityResultLauncher<Intent> getActivityLauncherOrReject(PluginCall call, String methodName) {
        ActivityResultLauncher<Intent> activityLauncher = activityLaunchers.get(methodName);

        // if there is no registered launcher, reject the call with an error and return null
        if (activityLauncher == null) {
            String registerError =
                "There is no ActivityCallback method registered for the name: %s. " +
                "Please define a callback method annotated with @ActivityCallback " +
                "that receives arguments: (PluginCall, ActivityResult)";
            registerError = String.format(Locale.US, registerError, methodName);
            Logger.error(registerError);
            call.reject(registerError);
            return null;
        }

        return activityLauncher;
    }

    /**
     * Gets the permission launcher associated with the calling methodName, or rejects the call if
     * no registered launcher exists
     *
     * @param call       the plugin call
     * @param methodName the name of the permission callback method
     * @return a launcher, or null if none found
     */
    private @Nullable ActivityResultLauncher<String[]> getPermissionLauncherOrReject(PluginCall call, String methodName) {
        ActivityResultLauncher<String[]> permissionLauncher = permissionLaunchers.get(methodName);

        // if there is no registered launcher, reject the call with an error and return null
        if (permissionLauncher == null) {
            String registerError =
                "There is no PermissionCallback method registered for the name: %s. " +
                "Please define a callback method annotated with @PermissionCallback " +
                "that receives arguments: (PluginCall)";
            registerError = String.format(Locale.US, registerError, methodName);
            Logger.error(registerError);
            call.reject(registerError);
            return null;
        }

        return permissionLauncher;
    }

    /**
     * Request all of the specified permissions in the CapacitorPlugin annotation (if any)
     *
     * @deprecated use {@link #requestAllPermissions(PluginCall, String)} in conjunction with @CapacitorPlugin
     */
    @Deprecated
    public void pluginRequestAllPermissions() {
        NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation();
        ActivityCompat.requestPermissions(getActivity(), legacyAnnotation.permissions(), legacyAnnotation.permissionRequestCode());
    }

    /**
     * Helper for requesting a specific permission
     *
     * @param permission  the permission to request
     * @param requestCode the requestCode to use to associate the result with the plugin
     * @deprecated use {@link #requestPermissionForAlias(String, PluginCall, String)} in conjunction with @CapacitorPlugin
     */
    @Deprecated
    public void pluginRequestPermission(String permission, int requestCode) {
        ActivityCompat.requestPermissions(getActivity(), new String[] { permission }, requestCode);
    }

    /**
     * Helper for requesting specific permissions
     * @deprecated use {@link #requestPermissionForAliases(String[], PluginCall, String)} in conjunction
     * with @CapacitorPlugin
     *
     * @param permissions the set of permissions to request
     * @param requestCode the requestCode to use to associate the result with the plugin
     */
    @Deprecated
    public void pluginRequestPermissions(String[] permissions, int requestCode) {
        ActivityCompat.requestPermissions(getActivity(), permissions, requestCode);
    }

    /**
     * Get the permission state for the provided permission alias.
     *
     * @param alias the permission alias to get
     * @return the state of the provided permission alias or null
     */
    public PermissionState getPermissionState(String alias) {
        return getPermissionStates().get(alias);
    }

    /**
     * Helper to check all permissions defined on a plugin and see the state of each.
     *
     * @since 3.0.0
     * @return A mapping of permission aliases to the associated granted status.
     */
    public Map<String, PermissionState> getPermissionStates() {
        return bridge.getPermissionStates(this);
    }

    /**
     * Add a listener for the given event
     * @param eventName
     * @param call
     */
    private void addEventListener(String eventName, PluginCall call) {
        List<PluginCall> listeners = eventListeners.get(eventName);
        if (listeners == null || listeners.isEmpty()) {
            listeners = new ArrayList<>();
            eventListeners.put(eventName, listeners);

            // Must add the call before sending retained arguments
            listeners.add(call);

            sendRetainedArgumentsForEvent(eventName);
        } else {
            listeners.add(call);
        }
    }

    /**
     * Remove a listener from the given event
     * @param eventName
     * @param call
     */
    private void removeEventListener(String eventName, PluginCall call) {
        List<PluginCall> listeners = eventListeners.get(eventName);
        if (listeners == null) {
            return;
        }

        listeners.remove(call);
    }

    /**
     * Notify all listeners that an event occurred
     * @param eventName
     * @param data
     */
    protected void notifyListeners(String eventName, JSObject data, boolean retainUntilConsumed) {
        Logger.verbose(getLogTag(), "Notifying listeners for event " + eventName);
        List<PluginCall> listeners = eventListeners.get(eventName);
        if (listeners == null || listeners.isEmpty()) {
            Logger.debug(getLogTag(), "No listeners found for event " + eventName);
            if (retainUntilConsumed) {
                List<JSObject> argList = retainedEventArguments.get(eventName);

                if (argList == null) {
                    argList = new ArrayList<JSObject>();
                }

                argList.add(data);
                retainedEventArguments.put(eventName, argList);
            }
            return;
        }

        CopyOnWriteArrayList<PluginCall> listenersCopy = new CopyOnWriteArrayList(listeners);
        for (PluginCall call : listenersCopy) {
            call.resolve(data);
        }
    }

    /**
     * Notify all listeners that an event occurred
     * This calls {@link Plugin#notifyListeners(String, JSObject, boolean)}
     * with retainUntilConsumed set to false
     * @param eventName
     * @param data
     */
    protected void notifyListeners(String eventName, JSObject data) {
        notifyListeners(eventName, data, false);
    }

    /**
     * Check if there are any listeners for the given event
     */
    protected boolean hasListeners(String eventName) {
        List<PluginCall> listeners = eventListeners.get(eventName);
        if (listeners == null) {
            return false;
        }
        return !listeners.isEmpty();
    }

    /**
     * Send retained arguments (if any) for this event. This
     * is called only when the first listener for an event is added
     * @param eventName
     */
    private void sendRetainedArgumentsForEvent(String eventName) {
        // copy retained args and null source to prevent potential race conditions
        List<JSObject> retainedArgs = retainedEventArguments.get(eventName);
        if (retainedArgs == null) {
            return;
        }

        retainedEventArguments.remove(eventName);

        for (JSObject retained : retainedArgs) {
            notifyListeners(eventName, retained);
        }
    }

    /**
     * Exported plugin call for adding a listener to this plugin
     * @param call
     */
    @SuppressWarnings("unused")
    @PluginMethod(returnType = PluginMethod.RETURN_NONE)
    public void addListener(PluginCall call) {
        String eventName = call.getString("eventName");
        call.setKeepAlive(true);
        addEventListener(eventName, call);
    }

    /**
     * Exported plugin call to remove a listener from this plugin
     * @param call
     */
    @SuppressWarnings("unused")
    @PluginMethod(returnType = PluginMethod.RETURN_NONE)
    public void removeListener(PluginCall call) {
        String eventName = call.getString("eventName");
        String callbackId = call.getString("callbackId");
        PluginCall savedCall = bridge.getSavedCall(callbackId);
        if (savedCall != null) {
            removeEventListener(eventName, savedCall);
            bridge.releaseCall(savedCall);
        }
    }

    /**
     * Exported plugin call to remove all listeners from this plugin
     * @param call
     */
    @SuppressWarnings("unused")
    @PluginMethod(returnType = PluginMethod.RETURN_PROMISE)
    public void removeAllListeners(PluginCall call) {
        eventListeners.clear();
        call.resolve();
    }

    /**
     * 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
    @PermissionCallback
    public void checkPermissions(PluginCall pluginCall) {
        Map<String, PermissionState> permissionsResult = getPermissionStates();

        if (permissionsResult.size() == 0) {
            // if no permissions are defined on the plugin, resolve undefined
            pluginCall.resolve();
        } else {
            JSObject permissionsResultJSON = new JSObject();
            for (Map.Entry<String, PermissionState> entry : permissionsResult.entrySet()) {
                permissionsResultJSON.put(entry.getKey(), entry.getValue());
            }

            pluginCall.resolve(permissionsResultJSON);
        }
    }

    /**
     * Exported plugin call to request all permissions for this plugin.
     * To manually request permissions within a plugin use:
     * {@link #requestAllPermissions(PluginCall, String)}, or
     * {@link #requestPermissionForAlias(String, PluginCall, String)}, or
     * {@link #requestPermissionForAliases(String[], PluginCall, String)}
     *
     * @param call the plugin call
     */
    @PluginMethod
    public void requestPermissions(PluginCall call) {
        CapacitorPlugin annotation = handle.getPluginAnnotation();
        if (annotation == null) {
            handleLegacyPermission(call);
        } else {
            // handle permission requests for plugins defined with @CapacitorPlugin (since 3.0.0)
            String[] permAliases = null;
            Set<String> autoGrantPerms = new HashSet<>();

            // If call was made with a list of specific permission aliases to request, save them
            // to be requested
            JSArray providedPerms = call.getArray("permissions");
            List<String> providedPermsList = null;

            if (providedPerms != null) {
                try {
                    providedPermsList = providedPerms.toList();
                } catch (JSONException ignore) {
                    // do nothing
                }
            }

            // If call was made without any custom permissions, request all from plugin annotation
            Set<String> aliasSet = new HashSet<>();
            if (providedPermsList == null || providedPermsList.isEmpty()) {
                for (Permission perm : annotation.permissions()) {
                    // If a permission is defined with no permission strings, separate it for auto-granting.
                    // Otherwise, the alias is added to the list to be requested.
                    if (perm.strings().length == 0 || (perm.strings().length == 1 && perm.strings()[0].isEmpty())) {
                        if (!perm.alias().isEmpty()) {
                            autoGrantPerms.add(perm.alias());
                        }
                    } else {
                        aliasSet.add(perm.alias());
                    }
                }

                permAliases = aliasSet.toArray(new String[0]);
            } else {
                for (Permission perm : annotation.permissions()) {
                    if (providedPermsList.contains(perm.alias())) {
                        aliasSet.add(perm.alias());
                    }
                }

                if (aliasSet.isEmpty()) {
                    call.reject("No valid permission alias was requested of this plugin.");
                } else {
                    permAliases = aliasSet.toArray(new String[0]);
                }
            }

            if (permAliases != null && permAliases.length > 0) {
                // request permissions using provided aliases or all defined on the plugin
                requestPermissionForAliases(permAliases, call, "checkPermissions");
            } else if (!autoGrantPerms.isEmpty()) {
                // if the plugin only has auto-grant permissions, return all as GRANTED
                JSObject permissionsResults = new JSObject();

                for (String perm : autoGrantPerms) {
                    permissionsResults.put(perm, PermissionState.GRANTED.toString());
                }

                call.resolve(permissionsResults);
            } else {
                // no permissions are defined on the plugin, resolve undefined
                call.resolve();
            }
        }
    }

    @SuppressWarnings("deprecation")
    private void handleLegacyPermission(PluginCall call) {
        // handle permission requests for plugins defined with @NativePlugin (prior to 3.0.0)
        NativePlugin legacyAnnotation = this.handle.getLegacyPluginAnnotation();
        String[] perms = legacyAnnotation.permissions();
        if (perms.length > 0) {
            saveCall(call);
            pluginRequestPermissions(perms, legacyAnnotation.permissionRequestCode());
        } else {
            call.resolve();
        }
    }

    /**
     * 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 callbacks 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 = PermissionHelper.getUndefinedPermissions(getContext(), permissions);
            for (String perm : missing) {
                builder.append(perm + "\n");
            }
            savedLastCall.reject(builder.toString());
            savedLastCall = null;
        }
    }

    /**
     * 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
     * method saves the full JSON blob of the options call. Since Bundle sizes
     * may be limited, plugins that expect to be called with large data
     * objects (such as a file), should override this method and selectively
     * store option values in a {@link Bundle} to avoid exceeding limits.
     * @return a new {@link Bundle} with fields set from the options of the last saved {@link PluginCall}
     */
    protected Bundle saveInstanceState() {
        PluginCall savedCall = bridge.getSavedCall(lastPluginCallId);

        if (savedCall == null) {
            return null;
        }

        Bundle ret = new Bundle();
        JSObject callData = savedCall.getData();

        if (callData != null) {
            ret.putString(BUNDLE_PERSISTED_OPTIONS_JSON_KEY, callData.toString());
        }

        return ret;
    }

    /**
     * Called when the app is opened with a previously un-handled
     * activity response. If the plugin that started the activity
     * stored data in {@link Plugin#saveInstanceState()} then this
     * method will be called to allow the plugin to restore from that.
     * @param state
     */
    protected void restoreState(Bundle state) {}

    /**
     * Handle activity result, should be overridden by each plugin
     *
     * @deprecated provide a callback method using the {@link ActivityCallback} annotation and use
     * the {@link #startActivityForResult(PluginCall, Intent, String)} method
     *
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Deprecated
    protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {}

    /**
     * Handle onNewIntent
     * @param intent
     */
    protected void handleOnNewIntent(Intent intent) {}

    /**
     * Handle onConfigurationChanged
     * @param newConfig
     */
    protected void handleOnConfigurationChanged(Configuration newConfig) {}

    /**
     * Handle onStart
     */
    protected void handleOnStart() {}

    /**
     * Handle onRestart
     */
    protected void handleOnRestart() {}

    /**
     * Handle onResume
     */
    protected void handleOnResume() {}

    /**
     * Handle onPause
     */
    protected void handleOnPause() {}

    /**
     * Handle onStop
     */
    protected void handleOnStop() {}

    /**
     * Handle onDestroy
     */
    protected void handleOnDestroy() {}

    /**
     * Give the plugins a chance to take control when a URL is about to be loaded in the WebView.
     * Returning true causes the WebView to abort loading the URL.
     * Returning false causes the WebView to continue loading the URL.
     * Returning null will defer to the default Capacitor policy
     */
    @SuppressWarnings("unused")
    public Boolean shouldOverrideLoad(Uri url) {
        return null;
    }

    /**
     * Start a new Activity.
     *
     * Note: This method must be used by all plugins instead of calling
     * {@link Activity#startActivityForResult} as it associates the plugin with
     * any resulting data from the new Activity even if this app
     * is destroyed by the OS (to free up memory, for example).
     * @param intent
     * @param resultCode
     */
    @Deprecated
    protected void startActivityForResult(PluginCall call, Intent intent, int resultCode) {
        bridge.startActivityForPluginWithResult(call, intent, resultCode);
    }

    /**
     * Execute the given runnable on the Bridge's task handler
     * @param runnable
     */
    public void execute(Runnable runnable) {
        bridge.execute(runnable);
    }

    /**
     * Shortcut for getting the plugin log tag
     * @param subTags
     */
    protected String getLogTag(String... subTags) {
        return Logger.tags(subTags);
    }

    /**
     * Gets a plugin log tag with the child's class name as subTag.
     */
    protected String getLogTag() {
        return Logger.tags(this.getClass().getSimpleName());
    }
}
