chromium/ui/android/java/src/org/chromium/ui/permissions/AndroidPermissionDelegateWithRequester.java

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.ui.permissions;

import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Process;
import android.util.SparseArray;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ContextUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * AndroidPermissionDelegate that implements much of the logic around requesting permissions.
 * Subclasses need to implement only the basic permissions checking and requesting methods.
 */
public abstract class AndroidPermissionDelegateWithRequester implements AndroidPermissionDelegate {
    private Handler mHandler;
    private SparseArray<PermissionRequestInfo> mOutstandingPermissionRequests;
    private int mNextRequestCode;

    // Constants used for permission request code bounding.
    private static final int REQUEST_CODE_PREFIX = 1000;
    private static final int REQUEST_CODE_RANGE_SIZE = 100;

    public AndroidPermissionDelegateWithRequester() {
        mHandler = new Handler();
        mOutstandingPermissionRequests = new SparseArray<PermissionRequestInfo>();
    }

    @Override
    public final boolean hasPermission(String permission) {
        boolean isGranted =
                ApiCompatibilityUtils.checkPermission(
                                ContextUtils.getApplicationContext(),
                                permission,
                                Process.myPid(),
                                Process.myUid())
                        == PackageManager.PERMISSION_GRANTED;
        if (isGranted) {
            PermissionPrefs.clearPermissionWasDenied(permission);
        }
        return isGranted;
    }

    @Override
    public final boolean canRequestPermission(String permission) {
        if (hasPermission(permission)) {
            // There is no need to call clearPermissionWasDenied - hasPermission already cleared
            // the shared pref if needed.
            return true;
        }

        if (isPermissionRevokedByPolicy(permission)) {
            return false;
        }

        if (shouldShowRequestPermissionRationale(permission)) {
            // This information from Android suggests we should not assume the user will always deny
            // the permission.
            PermissionPrefs.clearPermissionWasDenied(permission);
            return true;
        }

        // Check whether we have been denied this permission by checking whether we saved
        // a preference associated with it before. This is to identify the cases where the app never
        // requested for the permission before.
        return !PermissionPrefs.wasPermissionDenied(permission);
    }

    /** @see PackageManager#isPermissionRevokedByPolicy(String, String) */
    protected abstract boolean isPermissionRevokedByPolicyInternal(String permission);

    @Override
    public final boolean isPermissionRevokedByPolicy(String permission) {
        return isPermissionRevokedByPolicyInternal(permission);
    }

    @Override
    public final void requestPermissions(
            final String[] permissions, final PermissionCallback callback) {
        if (requestPermissionsInternal(permissions, callback)) {
            PermissionPrefs.onAndroidPermissionRequestUiShown(permissions);
            return;
        }

        // If the permission request was not sent successfully, just post a response to the
        // callback with whatever the current permission state is for all the requested
        // permissions.  The response is posted to keep the async behavior of this method
        // consistent.
        mHandler.post(
                new Runnable() {
                    @Override
                    public void run() {
                        int[] results = new int[permissions.length];
                        for (int i = 0; i < permissions.length; i++) {
                            results[i] =
                                    hasPermission(permissions[i])
                                            ? PackageManager.PERMISSION_GRANTED
                                            : PackageManager.PERMISSION_DENIED;
                        }
                        callback.onRequestPermissionsResult(permissions, results);
                    }
                });
    }

    @Override
    public final boolean handlePermissionResult(
            int requestCode, String[] permissions, int[] grantResults) {
        PermissionRequestInfo requestInfo = mOutstandingPermissionRequests.get(requestCode);
        mOutstandingPermissionRequests.delete(requestCode);

        List<String> permissionsGranted = new ArrayList<>();
        List<String> permissionsDenied = new ArrayList<>();
        for (int i = 0; i < permissions.length; i++) {
            if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                permissionsGranted.add(permissions[i]);
            } else if (shouldPersistDenial(requestInfo, permissions[i])) {
                permissionsDenied.add(permissions[i]);
            }
        }
        PermissionPrefs.editPermissionsPref(permissionsGranted, permissionsDenied);

        if (requestInfo == null || requestInfo.callback == null) return false;
        requestInfo.callback.onRequestPermissionsResult(permissions, grantResults);
        return true;
    }

    /**
     * Returns if an information about permission denial should be stored. Denial should not be
     * stored iff:
     * <ul>
     *   <li> Android version >= Android.R
     *   <li> The information about initial @see Activity.shouldShowRequestPermissionRationale is
     *        present and the value is false
     *   <li> The current value of @see Activity.shouldShowRequestPermissionRationale is false as
     *        well
     * </ul>
     */
    private boolean shouldPersistDenial(PermissionRequestInfo requestInfo, String permission) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return true;

        boolean initialShowRationaleState = false;
        if (requestInfo != null) {
            initialShowRationaleState = requestInfo.getInitialShowRationaleStateFor(permission);
        }

        return initialShowRationaleState || shouldShowRequestPermissionRationale(permission);
    }

    /** @see Activity.requestPermissions */
    protected abstract boolean requestPermissionsFromRequester(
            String[] permissions, int requestCode);

    /** Issues the permission request and returns whether it was sent successfully. */
    private boolean requestPermissionsInternal(String[] permissions, PermissionCallback callback) {
        int requestCode = REQUEST_CODE_PREFIX + mNextRequestCode;
        mNextRequestCode = (mNextRequestCode + 1) % REQUEST_CODE_RANGE_SIZE;
        mOutstandingPermissionRequests.put(
                requestCode, new PermissionRequestInfo(permissions, callback));
        if (!requestPermissionsFromRequester(permissions, requestCode)) {
            mOutstandingPermissionRequests.delete(requestCode);
            return false;
        }
        return true;
    }

    /** Wrapper holding information relevant to a permission request. */
    private class PermissionRequestInfo {
        public final PermissionCallback callback;
        public final Map<String, Boolean> initialShowRationaleState;

        public PermissionRequestInfo(String[] permissions, PermissionCallback callback) {
            initialShowRationaleState = new HashMap<>();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                for (String permission : permissions) {
                    initialShowRationaleState.put(
                            permission, shouldShowRequestPermissionRationale(permission));
                }
            }
            this.callback = callback;
        }

        /**
         * Returns initial value of @see Activity.shouldShowRequestPermissionRationale
         * for the given {@code permission} or false if not found.
         */
        public boolean getInitialShowRationaleStateFor(String permission) {
            assert initialShowRationaleState.get(permission) != null;
            return initialShowRationaleState.get(permission) != null
                    ? initialShowRationaleState.get(permission)
                    : false;
        }
    }
}