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

// Copyright 2022 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.Manifest;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;
import android.text.TextUtils;

import org.chromium.base.ContextUtils;
import org.chromium.base.TimeUtils;

import java.util.List;

/**
 * Provides helper methods for shared preference access to store permission request related info.
 */
public class PermissionPrefs {
    /**
     * Shared preference key prefix for remembering Android permissions denied by the user.
     * <p>
     * <b>NOTE:</b> As of M86 the semantics of shared prefs using this key prefix has changed:
     * <ul>
     *   <li>Previously: {@code true} if the user was ever asked for a permission, otherwise absent.
     *   <li>M86+: {@code true} if the user most recently has denied permission access,
     *     otherwise absent.
     * </ul>
     */
    private static final String PERMISSION_WAS_DENIED_KEY_PREFIX =
            "HasRequestedAndroidPermission::";

    /**
     * Shared preference key prefix for storing the timestamp of when the permission request was
     * shown. Only used for notification permission currently.
     */
    private static final String ANDROID_PERMISSION_REQUEST_TIMESTAMP_KEY_PREFIX =
            "AndroidPermissionRequestTimestamp::";

    /**
     * Returns normalized permission name for the given permission considering OS versions.
     * @param permission The permission name.
     * @return Normalized permission name.
     */
    public static String normalizePermissionName(String permission) {
        // Prior to O, permissions were granted at the group level.  Post O, each permission is
        // granted individually.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            try {
                // Runtime permissions are controlled at the group level.  So when determining
                // whether we have requested a particular permission before, we should check whether
                // we have requested any permission in that group as that mimics the logic in the
                // Android framework.
                //
                // e.g. Requesting first the permission ACCESS_FINE_LOCATION will result in Chrome
                //      treating ACCESS_COARSE_LOCATION as if it had already been requested as well.
                PermissionInfo permissionInfo =
                        ContextUtils.getApplicationContext()
                                .getPackageManager()
                                .getPermissionInfo(permission, PackageManager.GET_META_DATA);

                if (!TextUtils.isEmpty(permissionInfo.group)) {
                    return permissionInfo.group;
                }
            } catch (NameNotFoundException e) {
                // Unknown permission.  Default back to the permission name instead of the group.
            }
        }

        return permission;
    }

    /**
     * NOTE: Use this method with caution. The pref is aggressively cleared on
     * permission grant, or on shouldShowRequestPermissionRationale returning true.
     * @return Whether the request was denied by the user for the given {@code permission}
     */
    static boolean wasPermissionDenied(String permission) {
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        return prefs.getBoolean(getPermissionWasDeniedKey(permission), false);
    }

    /** Clear the shared pref indicating that {@code permission} was denied by the user. */
    static void clearPermissionWasDenied(String permission) {
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        SharedPreferences.Editor editor = prefs.edit();
        editor.remove(getPermissionWasDeniedKey(permission));
        editor.apply();
    }

    /**
     * Saves/deletes the given list of permissions from shared prefs. Permission entries are deleted
     * on permission grant and added on denial.
     * @param permissionsGranted The list of permissions to delete entries.
     * @param permissionsDenied The list of permissions to add/update entries.
     */
    static void editPermissionsPref(
            List<String> permissionsGranted, List<String> permissionsDenied) {
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        SharedPreferences.Editor editor = prefs.edit();
        for (String permission : permissionsGranted) {
            editor.remove(getPermissionWasDeniedKey(permission));
        }
        for (String permission : permissionsDenied) {
            editor.putBoolean(getPermissionWasDeniedKey(permission), true);
        }
        editor.apply();
    }

    /**
     * @return The timestamp when the notification permission request was shown last.
     */
    public static long getAndroidNotificationPermissionRequestTimestamp() {
        String prefName =
                ANDROID_PERMISSION_REQUEST_TIMESTAMP_KEY_PREFIX
                        + PermissionPrefs.normalizePermissionName(
                                Manifest.permission.POST_NOTIFICATIONS);
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        return prefs.getLong(prefName, 0);
    }

    /** Called when the android permission prompt was shown. */
    static void onAndroidPermissionRequestUiShown(String[] permissions) {
        boolean isNotification = false;
        for (String permission : permissions) {
            if (TextUtils.equals(permission, Manifest.permission.POST_NOTIFICATIONS)) {
                isNotification = true;
                break;
            }
        }
        if (!isNotification) return;

        String prefName =
                ANDROID_PERMISSION_REQUEST_TIMESTAMP_KEY_PREFIX
                        + PermissionPrefs.normalizePermissionName(
                                Manifest.permission.POST_NOTIFICATIONS);
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        prefs.edit().putLong(prefName, TimeUtils.currentTimeMillis()).apply();
    }

    /**
     * Returns the name of a shared preferences key used to store whether Chrome was denied
     * {@code permission}.
     */
    private static String getPermissionWasDeniedKey(String permission) {
        return PERMISSION_WAS_DENIED_KEY_PREFIX + normalizePermissionName(permission);
    }
}