// Copyright 2020 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.components.permissions;
import android.Manifest;
import android.os.Build;
import androidx.core.app.NotificationManagerCompat;
import org.jni_zero.CalledByNative;
import org.chromium.base.ContextUtils;
import org.chromium.base.PackageManagerUtils;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.components.location.LocationUtils;
import org.chromium.components.webxr.WebXrAndroidFeatureMap;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.permissions.ContextualNotificationPermissionRequester;
import org.chromium.ui.permissions.PermissionCallback;
import java.util.Arrays;
/** A utility class for permissions. */
public class PermissionUtil {
/**
* TODO(https://crbug.com/331574787): Replace with official strings. At which time, any
* additional checks being done to guard this with the immersive feature can likely also be
* removed.
*/
public static final String ANDROID_PERMISSION_SCENE_UNDERSTANDING =
"android.permission.SCENE_UNDERSTANDING";
public static final String ANDROID_PERMISSION_HAND_TRACKING =
"android.permission.HAND_TRACKING";
/** The permissions associated with requesting location pre-Android S. */
private static final String[] LOCATION_PERMISSIONS_PRE_S = {
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION
};
/** The required Android permissions associated with requesting location post-Android S. */
private static final String[] LOCATION_REQUIRED_PERMISSIONS_POST_S = {
android.Manifest.permission.ACCESS_COARSE_LOCATION
};
/** The optional Android permissions associated with requesting location post-Android S. */
private static final String[] LOCATION_OPTIONAL_PERMISSIONS_POST_S = {
android.Manifest.permission.ACCESS_FINE_LOCATION
};
/** The android permissions associated with requesting access to the camera. */
private static final String[] CAMERA_PERMISSIONS = {android.Manifest.permission.CAMERA};
/** The android permissions associated with requesting access to the microphone. */
private static final String[] MICROPHONE_PERMISSIONS = {
android.Manifest.permission.RECORD_AUDIO
};
/** The required android permissions associated with posting notifications post-Android T. */
private static final String[] NOTIFICATION_PERMISSIONS_POST_T = {
android.Manifest.permission.POST_NOTIFICATIONS
};
private static final String[] OPENXR_PERMISSIONS = {ANDROID_PERMISSION_SCENE_UNDERSTANDING};
private static final String[] HAND_TRACKING_PERMISSIONS = {ANDROID_PERMISSION_HAND_TRACKING};
/** Signifies there are no permissions associated. */
private static final String[] EMPTY_PERMISSIONS = {};
private PermissionUtil() {}
/** Whether precise/approximate location support is enabled. */
private static boolean isApproximateLocationSupportEnabled() {
// Even for apps targeting SDK version 30-, the user can downgrade location precision
// in app settings if the device is running Android S. In addition, for apps targeting SDK
// version 31+, users will be able to choose the precision in the permission dialog for apps
// targeting SDK version 31. Therefore enable support based on the current device's
// software's SDK version as opposed to Chrome's targetSdkVersion. See:
// https://developer.android.com/about/versions/12/approximate-location
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& PermissionsAndroidFeatureMap.isEnabled(
PermissionsAndroidFeatureList
.ANDROID_APPROXIMATE_LOCATION_PERMISSION_SUPPORT);
}
private static boolean hasImmersiveFeature() {
return PackageManagerUtils.hasSystemFeature(PackageManagerUtils.XR_IMMERSIVE_FEATURE_NAME);
}
private static boolean isOpenXrSupportEnabled() {
// OpenXR only requires additional permissions after Android 14.
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
&& hasImmersiveFeature()
&& WebXrAndroidFeatureMap.isOpenXrEnabled();
}
/**
* Returns required Android permission strings for a given {@link ContentSettingsType}. If there
* is no permissions associated with the content setting, then an empty array is returned.
*
* @param contentSettingType The content setting to get the Android permissions for.
* @return The required Android permissions for the given content setting. Permission sets
* returned for different content setting types are disjunct.
*/
@CalledByNative
public static String[] getRequiredAndroidPermissionsForContentSetting(int contentSettingType) {
switch (contentSettingType) {
case ContentSettingsType.GEOLOCATION:
if (isApproximateLocationSupportEnabled()) {
return Arrays.copyOf(
LOCATION_REQUIRED_PERMISSIONS_POST_S,
LOCATION_REQUIRED_PERMISSIONS_POST_S.length);
}
return Arrays.copyOf(LOCATION_PERMISSIONS_PRE_S, LOCATION_PERMISSIONS_PRE_S.length);
case ContentSettingsType.MEDIASTREAM_MIC:
return Arrays.copyOf(MICROPHONE_PERMISSIONS, MICROPHONE_PERMISSIONS.length);
case ContentSettingsType.MEDIASTREAM_CAMERA:
return Arrays.copyOf(CAMERA_PERMISSIONS, CAMERA_PERMISSIONS.length);
case ContentSettingsType.AR:
if (isOpenXrSupportEnabled()) {
return Arrays.copyOf(OPENXR_PERMISSIONS, OPENXR_PERMISSIONS.length);
}
return Arrays.copyOf(CAMERA_PERMISSIONS, CAMERA_PERMISSIONS.length);
case ContentSettingsType.VR:
if (isOpenXrSupportEnabled()) {
return Arrays.copyOf(OPENXR_PERMISSIONS, OPENXR_PERMISSIONS.length);
}
return EMPTY_PERMISSIONS;
case ContentSettingsType.HAND_TRACKING:
if (hasImmersiveFeature() && WebXrAndroidFeatureMap.isHandTrackingEnabled()) {
return Arrays.copyOf(
HAND_TRACKING_PERMISSIONS, HAND_TRACKING_PERMISSIONS.length);
}
return EMPTY_PERMISSIONS;
case ContentSettingsType.NOTIFICATIONS:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return Arrays.copyOf(
NOTIFICATION_PERMISSIONS_POST_T,
NOTIFICATION_PERMISSIONS_POST_T.length);
}
return EMPTY_PERMISSIONS;
default:
return EMPTY_PERMISSIONS;
}
}
/**
* Returns optional Android permission strings for a given {@link ContentSettingsType}. If
* there is no permissions associated with the content setting, or all of them are required,
* then an empty array is returned.
*
* @param contentSettingType The content setting to get the Android permissions for.
* @return The optional Android permissions for the given content setting. Permission sets
* returned for different content setting types are disjunct.
*/
@CalledByNative
public static String[] getOptionalAndroidPermissionsForContentSetting(int contentSettingType) {
switch (contentSettingType) {
case ContentSettingsType.GEOLOCATION:
if (isApproximateLocationSupportEnabled()) {
return Arrays.copyOf(
LOCATION_OPTIONAL_PERMISSIONS_POST_S,
LOCATION_OPTIONAL_PERMISSIONS_POST_S.length);
}
return EMPTY_PERMISSIONS;
default:
return EMPTY_PERMISSIONS;
}
}
@CalledByNative
private static boolean doesAppLevelSettingsAllowSiteNotifications() {
ContextualNotificationPermissionRequester contextualPermissionRequester =
ContextualNotificationPermissionRequester.getInstance();
return contextualPermissionRequester != null
&& contextualPermissionRequester.doesAppLevelSettingsAllowSiteNotifications();
}
@CalledByNative
private static boolean areAppLevelNotificationsEnabled() {
NotificationManagerCompat manager =
NotificationManagerCompat.from(ContextUtils.getApplicationContext());
return manager.areNotificationsEnabled();
}
public static boolean hasSystemPermissionsForBluetooth(WindowAndroid windowAndroid) {
return !needsNearbyDevicesPermissionForBluetooth(windowAndroid)
&& !needsLocationPermissionForBluetooth(windowAndroid);
}
@CalledByNative
public static boolean needsLocationPermissionForBluetooth(WindowAndroid windowAndroid) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.S
&& !windowAndroid.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION);
}
@CalledByNative
public static boolean needsNearbyDevicesPermissionForBluetooth(WindowAndroid windowAndroid) {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& (!windowAndroid.hasPermission(Manifest.permission.BLUETOOTH_SCAN)
|| !windowAndroid.hasPermission(Manifest.permission.BLUETOOTH_CONNECT));
}
@CalledByNative
public static boolean needsLocationServicesForBluetooth() {
// Location services are not required on Android S+ to use Bluetooth if the application has
// Nearby Devices permission and has set the neverForLocation flag on the BLUETOOTH_SCAN
// permission in its manifest.
return Build.VERSION.SDK_INT < Build.VERSION_CODES.S
&& !LocationUtils.getInstance().isSystemLocationSettingEnabled();
}
@CalledByNative
public static boolean canRequestSystemPermissionsForBluetooth(WindowAndroid windowAndroid) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return windowAndroid.canRequestPermission(Manifest.permission.BLUETOOTH_SCAN)
&& windowAndroid.canRequestPermission(Manifest.permission.BLUETOOTH_CONNECT);
}
return windowAndroid.canRequestPermission(Manifest.permission.ACCESS_FINE_LOCATION);
}
@CalledByNative
public static void requestSystemPermissionsForBluetooth(
WindowAndroid windowAndroid, PermissionCallback callback) {
String[] requiredPermissions;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requiredPermissions =
new String[] {
Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT
};
} else {
requiredPermissions = new String[] {Manifest.permission.ACCESS_FINE_LOCATION};
}
// TODO(crbug.com/40255210): Removes this checking for null callback.
if (callback == null) {
callback = (permissions, grantResults) -> {};
}
windowAndroid.requestPermissions(requiredPermissions, callback);
}
@CalledByNative
public static void requestLocationServices(WindowAndroid windowAndroid) {
windowAndroid
.getActivity()
.get()
.startActivity(LocationUtils.getInstance().getSystemLocationSettingsIntent());
}
}