/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.permissioncontroller.permission.utils; import static android.Manifest.permission_group.ACTIVITY_RECOGNITION; import static android.Manifest.permission_group.CALENDAR; import static android.Manifest.permission_group.CALL_LOG; import static android.Manifest.permission_group.CAMERA; import static android.Manifest.permission_group.CONTACTS; import static android.Manifest.permission_group.LOCATION; import static android.Manifest.permission_group.MICROPHONE; import static android.Manifest.permission_group.NEARBY_DEVICES; import static android.Manifest.permission_group.NOTIFICATIONS; import static android.Manifest.permission_group.PHONE; import static android.Manifest.permission_group.READ_MEDIA_AURAL; import static android.Manifest.permission_group.READ_MEDIA_VISUAL; import static android.Manifest.permission_group.SENSORS; import static android.Manifest.permission_group.SMS; import static android.Manifest.permission_group.STORAGE; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE; import static android.content.Context.MODE_PRIVATE; import static android.content.Intent.EXTRA_PACKAGE_NAME; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED; import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED; import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; import static android.health.connect.HealthConnectManager.ACTION_MANAGE_HEALTH_PERMISSIONS; import static android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP; import static android.os.UserHandle.myUserId; import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.Manifest; import android.app.AppOpsManager; import android.app.Application; import android.app.admin.DevicePolicyManager; import android.app.ecm.EnhancedConfirmationManager; import android.app.role.RoleManager; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; import android.content.pm.UserProperties; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.hardware.SensorPrivacyManager; import android.health.connect.HealthConnectManager; import android.os.Binder; import android.os.Build; import android.os.Parcelable; import android.os.UserHandle; import android.os.UserManager; import android.permission.flags.Flags; import android.provider.DeviceConfig; import android.provider.Settings; import android.text.Html; import android.text.TextUtils; import android.text.format.DateFormat; import android.util.ArrayMap; import android.util.Log; import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.ColorRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.core.text.BidiFormatter; import androidx.core.util.Preconditions; import com.android.launcher3.icons.IconFactory; import com.android.modules.utils.build.SdkLevel; import com.android.permissioncontroller.Constants; import com.android.permissioncontroller.DeviceUtils; import com.android.permissioncontroller.PermissionControllerApplication; import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.model.AppPermissionGroup; import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup; import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo; import kotlin.Triple; import java.lang.annotation.Retention; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Calendar; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Random; import java.util.Set; public final class Utils { @Retention(SOURCE) @IntDef(value = {LAST_24H_SENSOR_TODAY, LAST_24H_SENSOR_YESTERDAY, LAST_24H_CONTENT_PROVIDER, NOT_IN_LAST_7D}) public @interface AppPermsLastAccessType {} public static final int LAST_24H_SENSOR_TODAY = 1; public static final int LAST_24H_SENSOR_YESTERDAY = 2; public static final int LAST_24H_CONTENT_PROVIDER = 3; public static final int LAST_7D_SENSOR = 4; public static final int LAST_7D_CONTENT_PROVIDER = 5; public static final int NOT_IN_LAST_7D = 6; private static final String LOG_TAG = "Utils"; public static final String OS_PKG = "android"; public static final float DEFAULT_MAX_LABEL_SIZE_PX = 500f; /** The time an app needs to be unused in order to be hibernated */ public static final String PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS = "auto_revoke_unused_threshold_millis2"; /** The frequency of running the job for hibernating apps */ public static final String PROPERTY_HIBERNATION_CHECK_FREQUENCY_MILLIS = "auto_revoke_check_frequency_millis"; /** Whether hibernation targets apps that target a pre-S SDK */ public static final String PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS = "app_hibernation_targets_pre_s_apps"; /** Whether or not app hibernation is enabled on the device **/ public static final String PROPERTY_APP_HIBERNATION_ENABLED = "app_hibernation_enabled"; /** Whether the system exempt from hibernation is enabled on the device **/ public static final String PROPERTY_SYSTEM_EXEMPT_HIBERNATION_ENABLED = "system_exempt_hibernation_enabled"; /** The timeout for one-time permissions */ private static final String PROPERTY_ONE_TIME_PERMISSIONS_TIMEOUT_MILLIS = "one_time_permissions_timeout_millis"; /** The delay before ending a one-time permission session when all processes are dead */ private static final String PROPERTY_ONE_TIME_PERMISSIONS_KILLED_DELAY_MILLIS = "one_time_permissions_killed_delay_millis"; /** Whether to show health permission in various permission controller UIs. */ private static final String PROPERTY_HEALTH_PERMISSION_UI_ENABLED = "health_permission_ui_enabled"; /** How frequently to check permission event store to scrub old data */ public static final String PROPERTY_PERMISSION_EVENTS_CHECK_OLD_FREQUENCY_MILLIS = "permission_events_check_old_frequency_millis"; /** * Whether to store the exact time for permission changes. Only for use in tests and should * not be modified in prod. */ public static final String PROPERTY_PERMISSION_CHANGES_STORE_EXACT_TIME = "permission_changes_store_exact_time"; /** The max amount of time permission data can stay in the storage before being scrubbed */ public static final String PROPERTY_PERMISSION_DECISIONS_MAX_DATA_AGE_MILLIS = "permission_decisions_max_data_age_millis"; /** All permission whitelists. */ public static final int FLAGS_PERMISSION_WHITELIST_ALL = PackageManager.FLAG_PERMISSION_WHITELIST_SYSTEM | PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE | PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER; /** All permission restriction exemptions. */ public static final int FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT = FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT | FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT | FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; /** * The default length of the timeout for one-time permissions */ public static final long ONE_TIME_PERMISSIONS_TIMEOUT_MILLIS = 1 * 60 * 1000; // 1 minute /** * The default length to wait before ending a one-time permission session after all processes * are dead. */ public static final long ONE_TIME_PERMISSIONS_KILLED_DELAY_MILLIS = 5 * 1000; private static final ArrayMap PERM_GROUP_REQUEST_RES; private static final ArrayMap PERM_GROUP_REQUEST_DEVICE_AWARE_RES; private static final ArrayMap PERM_GROUP_REQUEST_DETAIL_RES; private static final ArrayMap PERM_GROUP_BACKGROUND_REQUEST_RES; private static final ArrayMap PERM_GROUP_BACKGROUND_REQUEST_DEVICE_AWARE_RES; private static final ArrayMap PERM_GROUP_BACKGROUND_REQUEST_DETAIL_RES; private static final ArrayMap PERM_GROUP_UPGRADE_REQUEST_RES; private static final ArrayMap PERM_GROUP_UPGRADE_REQUEST_DEVICE_AWARE_RES; private static final ArrayMap PERM_GROUP_UPGRADE_REQUEST_DETAIL_RES; /** Permission -> Sensor codes */ private static final ArrayMap PERM_SENSOR_CODES; /** Permission -> Icon res id */ private static final ArrayMap PERM_BLOCKED_ICON; /** Permission -> Title res id */ private static final ArrayMap PERM_BLOCKED_TITLE; /** Permission -> Title res id */ private static final ArrayMap PERM_BLOCKED_TITLE_AUTOMOTIVE; public static final int FLAGS_ALWAYS_USER_SENSITIVE = FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED | FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED; private static final String SYSTEM_AMBIENT_AUDIO_INTELLIGENCE = "android.app.role.SYSTEM_AMBIENT_AUDIO_INTELLIGENCE"; private static final String SYSTEM_UI_INTELLIGENCE = "android.app.role.SYSTEM_UI_INTELLIGENCE"; private static final String SYSTEM_AUDIO_INTELLIGENCE = "android.app.role.SYSTEM_AUDIO_INTELLIGENCE"; private static final String SYSTEM_NOTIFICATION_INTELLIGENCE = "android.app.role.SYSTEM_NOTIFICATION_INTELLIGENCE"; private static final String SYSTEM_TEXT_INTELLIGENCE = "android.app.role.SYSTEM_TEXT_INTELLIGENCE"; private static final String SYSTEM_VISUAL_INTELLIGENCE = "android.app.role.SYSTEM_VISUAL_INTELLIGENCE"; // TODO: theianchen Using hardcoded values here as a WIP solution for now. private static final String[] EXEMPTED_ROLES = { SYSTEM_AMBIENT_AUDIO_INTELLIGENCE, SYSTEM_UI_INTELLIGENCE, SYSTEM_AUDIO_INTELLIGENCE, SYSTEM_NOTIFICATION_INTELLIGENCE, SYSTEM_TEXT_INTELLIGENCE, SYSTEM_VISUAL_INTELLIGENCE, }; static { PERM_GROUP_REQUEST_RES = new ArrayMap<>(); PERM_GROUP_REQUEST_RES.put(CONTACTS, R.string.permgrouprequest_contacts); PERM_GROUP_REQUEST_RES.put(LOCATION, R.string.permgrouprequest_location); PERM_GROUP_REQUEST_RES.put(NEARBY_DEVICES, R.string.permgrouprequest_nearby_devices); PERM_GROUP_REQUEST_RES.put(CALENDAR, R.string.permgrouprequest_calendar); PERM_GROUP_REQUEST_RES.put(SMS, R.string.permgrouprequest_sms); PERM_GROUP_REQUEST_RES.put(STORAGE, R.string.permgrouprequest_storage); PERM_GROUP_REQUEST_RES.put(READ_MEDIA_AURAL, R.string.permgrouprequest_read_media_aural); PERM_GROUP_REQUEST_RES.put(READ_MEDIA_VISUAL, R.string.permgrouprequest_read_media_visual); PERM_GROUP_REQUEST_RES.put(MICROPHONE, R.string.permgrouprequest_microphone); PERM_GROUP_REQUEST_RES .put(ACTIVITY_RECOGNITION, R.string.permgrouprequest_activityRecognition); PERM_GROUP_REQUEST_RES.put(CAMERA, R.string.permgrouprequest_camera); PERM_GROUP_REQUEST_RES.put(CALL_LOG, R.string.permgrouprequest_calllog); PERM_GROUP_REQUEST_RES.put(PHONE, R.string.permgrouprequest_phone); PERM_GROUP_REQUEST_RES.put(SENSORS, R.string.permgrouprequest_sensors); PERM_GROUP_REQUEST_RES.put(NOTIFICATIONS, R.string.permgrouprequest_notifications); PERM_GROUP_REQUEST_DEVICE_AWARE_RES = new ArrayMap<>(); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(CONTACTS, R.string.permgrouprequest_device_aware_contacts); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(LOCATION, R.string.permgrouprequest_device_aware_location); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(NEARBY_DEVICES, R.string.permgrouprequest_device_aware_nearby_devices); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(CALENDAR, R.string.permgrouprequest_device_aware_calendar); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(SMS, R.string.permgrouprequest_device_aware_sms); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(STORAGE, R.string.permgrouprequest_device_aware_storage); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(READ_MEDIA_AURAL, R.string.permgrouprequest_device_aware_read_media_aural); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(READ_MEDIA_VISUAL, R.string.permgrouprequest_device_aware_read_media_visual); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(MICROPHONE, R.string.permgrouprequest_device_aware_microphone); PERM_GROUP_REQUEST_DEVICE_AWARE_RES .put(ACTIVITY_RECOGNITION, R.string.permgrouprequest_device_aware_activityRecognition); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(CAMERA, R.string.permgrouprequest_device_aware_camera); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(CALL_LOG, R.string.permgrouprequest_device_aware_calllog); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(PHONE, R.string.permgrouprequest_device_aware_phone); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(SENSORS, R.string.permgrouprequest_device_aware_sensors); PERM_GROUP_REQUEST_DEVICE_AWARE_RES.put(NOTIFICATIONS, R.string.permgrouprequest_device_aware_notifications); PERM_GROUP_REQUEST_DETAIL_RES = new ArrayMap<>(); PERM_GROUP_REQUEST_DETAIL_RES.put(LOCATION, R.string.permgrouprequestdetail_location); PERM_GROUP_REQUEST_DETAIL_RES.put(MICROPHONE, R.string.permgrouprequestdetail_microphone); PERM_GROUP_REQUEST_DETAIL_RES.put(CAMERA, R.string.permgrouprequestdetail_camera); PERM_GROUP_BACKGROUND_REQUEST_RES = new ArrayMap<>(); PERM_GROUP_BACKGROUND_REQUEST_RES .put(LOCATION, R.string.permgroupbackgroundrequest_location); PERM_GROUP_BACKGROUND_REQUEST_RES .put(MICROPHONE, R.string.permgroupbackgroundrequest_microphone); PERM_GROUP_BACKGROUND_REQUEST_RES .put(CAMERA, R.string.permgroupbackgroundrequest_camera); PERM_GROUP_BACKGROUND_REQUEST_RES .put(SENSORS, R.string.permgroupbackgroundrequest_sensors); PERM_GROUP_BACKGROUND_REQUEST_DEVICE_AWARE_RES = new ArrayMap<>(); PERM_GROUP_BACKGROUND_REQUEST_DEVICE_AWARE_RES .put(LOCATION, R.string.permgroupbackgroundrequest_device_aware_location); PERM_GROUP_BACKGROUND_REQUEST_DEVICE_AWARE_RES .put(MICROPHONE, R.string.permgroupbackgroundrequest_device_aware_microphone); PERM_GROUP_BACKGROUND_REQUEST_DEVICE_AWARE_RES .put(CAMERA, R.string.permgroupbackgroundrequest_device_aware_camera); PERM_GROUP_BACKGROUND_REQUEST_DEVICE_AWARE_RES .put(SENSORS, R.string.permgroupbackgroundrequest_device_aware_sensors); PERM_GROUP_BACKGROUND_REQUEST_DETAIL_RES = new ArrayMap<>(); PERM_GROUP_BACKGROUND_REQUEST_DETAIL_RES .put(LOCATION, R.string.permgroupbackgroundrequestdetail_location); PERM_GROUP_BACKGROUND_REQUEST_DETAIL_RES .put(MICROPHONE, R.string.permgroupbackgroundrequestdetail_microphone); PERM_GROUP_BACKGROUND_REQUEST_DETAIL_RES .put(CAMERA, R.string.permgroupbackgroundrequestdetail_camera); PERM_GROUP_BACKGROUND_REQUEST_DETAIL_RES .put(SENSORS, R.string.permgroupbackgroundrequestdetail_sensors); PERM_GROUP_UPGRADE_REQUEST_RES = new ArrayMap<>(); PERM_GROUP_UPGRADE_REQUEST_RES.put(LOCATION, R.string.permgroupupgraderequest_location); PERM_GROUP_UPGRADE_REQUEST_RES.put(MICROPHONE, R.string.permgroupupgraderequest_microphone); PERM_GROUP_UPGRADE_REQUEST_RES.put(CAMERA, R.string.permgroupupgraderequest_camera); PERM_GROUP_UPGRADE_REQUEST_RES.put(SENSORS, R.string.permgroupupgraderequest_sensors); PERM_GROUP_UPGRADE_REQUEST_DEVICE_AWARE_RES = new ArrayMap<>(); PERM_GROUP_UPGRADE_REQUEST_DEVICE_AWARE_RES.put(LOCATION, R.string.permgroupupgraderequest_device_aware_location); PERM_GROUP_UPGRADE_REQUEST_DEVICE_AWARE_RES.put(MICROPHONE, R.string.permgroupupgraderequest_device_aware_microphone); PERM_GROUP_UPGRADE_REQUEST_DEVICE_AWARE_RES.put(CAMERA, R.string.permgroupupgraderequest_device_aware_camera); PERM_GROUP_UPGRADE_REQUEST_DEVICE_AWARE_RES.put(SENSORS, R.string.permgroupupgraderequest_device_aware_sensors); PERM_GROUP_UPGRADE_REQUEST_DETAIL_RES = new ArrayMap<>(); PERM_GROUP_UPGRADE_REQUEST_DETAIL_RES .put(LOCATION, R.string.permgroupupgraderequestdetail_location); PERM_GROUP_UPGRADE_REQUEST_DETAIL_RES .put(MICROPHONE, R.string.permgroupupgraderequestdetail_microphone); PERM_GROUP_UPGRADE_REQUEST_DETAIL_RES .put(CAMERA, R.string.permgroupupgraderequestdetail_camera); PERM_GROUP_UPGRADE_REQUEST_DETAIL_RES .put(SENSORS, R.string.permgroupupgraderequestdetail_sensors); PERM_SENSOR_CODES = new ArrayMap<>(); if (SdkLevel.isAtLeastS()) { PERM_SENSOR_CODES.put(CAMERA, SensorPrivacyManager.Sensors.CAMERA); PERM_SENSOR_CODES.put(MICROPHONE, SensorPrivacyManager.Sensors.MICROPHONE); } PERM_BLOCKED_ICON = new ArrayMap<>(); PERM_BLOCKED_ICON.put(CAMERA, R.drawable.ic_camera_blocked); PERM_BLOCKED_ICON.put(MICROPHONE, R.drawable.ic_mic_blocked); PERM_BLOCKED_ICON.put(LOCATION, R.drawable.ic_location_blocked); PERM_BLOCKED_TITLE = new ArrayMap<>(); PERM_BLOCKED_TITLE.put(CAMERA, R.string.blocked_camera_title); PERM_BLOCKED_TITLE.put(MICROPHONE, R.string.blocked_microphone_title); PERM_BLOCKED_TITLE.put(LOCATION, R.string.blocked_location_title); PERM_BLOCKED_TITLE_AUTOMOTIVE = new ArrayMap<>(); PERM_BLOCKED_TITLE_AUTOMOTIVE.put(CAMERA, R.string.automotive_blocked_camera_title); PERM_BLOCKED_TITLE_AUTOMOTIVE.put(MICROPHONE, R.string.automotive_blocked_microphone_title); PERM_BLOCKED_TITLE_AUTOMOTIVE.put(LOCATION, R.string.automotive_blocked_location_title); } private Utils() { /* do nothing - hide constructor */ } private static Object sLock = new Object(); private static ArrayMap sUserContexts = new ArrayMap<>(); /** * Creates and caches a PackageContext for the requested user, or returns the previously cached * value. The package of the PackageContext is the application's package. * * @param context The context of the currently running application * @param user The desired user for the context * * @return The generated or cached Context for the requested user * * @throws RuntimeException If the app has no package name attached, which should never happen */ public static @NonNull Context getUserContext(Context context, UserHandle user) { synchronized (sLock) { if (!sUserContexts.containsKey(user)) { sUserContexts.put(user, context.getApplicationContext() .createContextAsUser(user, 0)); } return Preconditions.checkNotNull(sUserContexts.get(user)); } } /** * {@code @NonNull} version of {@link Context#getSystemService(Class)} */ public static @NonNull M getSystemServiceSafe(@NonNull Context context, Class clazz) { return Preconditions.checkNotNull(context.getSystemService(clazz), "Could not resolve " + clazz.getSimpleName()); } /** * {@code @NonNull} version of {@link Context#getSystemService(Class)} */ public static @NonNull M getSystemServiceSafe(@NonNull Context context, Class clazz, @NonNull UserHandle user) { try { return Preconditions.checkNotNull(context.createPackageContextAsUser( context.getPackageName(), 0, user).getSystemService(clazz), "Could not resolve " + clazz.getSimpleName()); } catch (PackageManager.NameNotFoundException neverHappens) { throw new IllegalStateException(); } } /** * {@code @NonNull} version of {@link Intent#getParcelableExtra(String)} */ public static @NonNull T getParcelableExtraSafe(@NonNull Intent intent, @NonNull String name) { return Preconditions.checkNotNull(intent.getParcelableExtra(name), "Could not get parcelable extra for " + name); } /** * {@code @NonNull} version of {@link Intent#getStringExtra(String)} */ public static @NonNull String getStringExtraSafe(@NonNull Intent intent, @NonNull String name) { return Preconditions.checkNotNull(intent.getStringExtra(name), "Could not get string extra for " + name); } /** * Returns true if a permission is dangerous, installed, and not removed * @param permissionInfo The permission we wish to check * @return If all of the conditions are met */ public static boolean isPermissionDangerousInstalledNotRemoved(PermissionInfo permissionInfo) { return permissionInfo != null && permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS && (permissionInfo.flags & PermissionInfo.FLAG_INSTALLED) != 0 && (permissionInfo.flags & PermissionInfo.FLAG_REMOVED) == 0; } /** * Get the {@link PermissionInfo infos} for all permission infos belonging to a group. * * @param pm Package manager to use to resolve permission infos * @param group the group * * @return The infos of permissions belonging to the group or an empty list if the group * does not have runtime permissions */ public static @NonNull List getPermissionInfosForGroup( @NonNull PackageManager pm, @NonNull String group) throws PackageManager.NameNotFoundException { List permissions = pm.queryPermissionsByGroup(group, 0); permissions.addAll(PermissionMapping.getPlatformPermissionsOfGroup(pm, group)); /* * If the undefined group is requested, the package manager will return all platform * permissions, since they are marked as Undefined in the manifest. Do not return these * permissions. */ if (group.equals(Manifest.permission_group.UNDEFINED)) { List undefinedPerms = new ArrayList<>(); for (PermissionInfo permissionInfo : permissions) { String permGroup = PermissionMapping.getGroupOfPlatformPermission(permissionInfo.name); if (permGroup == null || permGroup.equals(Manifest.permission_group.UNDEFINED)) { undefinedPerms.add(permissionInfo); } } return undefinedPerms; } return permissions; } /** * Get the {@link PermissionInfo infos} for all runtime installed permission infos belonging to * a group. * * @param pm Package manager to use to resolve permission infos * @param group the group * * @return The infos of installed runtime permissions belonging to the group or an empty list * if the group does not have runtime permissions */ public static @NonNull List getInstalledRuntimePermissionInfosForGroup( @NonNull PackageManager pm, @NonNull String group) throws PackageManager.NameNotFoundException { List permissions = pm.queryPermissionsByGroup(group, 0); permissions.addAll(PermissionMapping.getPlatformPermissionsOfGroup(pm, group)); List installedRuntime = new ArrayList<>(); for (PermissionInfo permissionInfo: permissions) { if (permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS && (permissionInfo.flags & PermissionInfo.FLAG_INSTALLED) != 0 && (permissionInfo.flags & PermissionInfo.FLAG_REMOVED) == 0) { installedRuntime.add(permissionInfo); } } /* * If the undefined group is requested, the package manager will return all platform * permissions, since they are marked as Undefined in the manifest. Do not return these * permissions. */ if (group.equals(Manifest.permission_group.UNDEFINED)) { List undefinedPerms = new ArrayList<>(); for (PermissionInfo permissionInfo : installedRuntime) { String permGroup = PermissionMapping.getGroupOfPlatformPermission(permissionInfo.name); if (permGroup == null || permGroup.equals(Manifest.permission_group.UNDEFINED)) { undefinedPerms.add(permissionInfo); } } return undefinedPerms; } return installedRuntime; } /** * Get the {@link PackageItemInfo infos} for the given permission group. * * @param groupName the group * @param context the {@code Context} to retrieve {@code PackageManager} * * @return The info of permission group or null if the group does not have runtime permissions. */ public static @Nullable PackageItemInfo getGroupInfo(@NonNull String groupName, @NonNull Context context) { try { return context.getPackageManager().getPermissionGroupInfo(groupName, 0); } catch (NameNotFoundException e) { /* ignore */ } try { return context.getPackageManager().getPermissionInfo(groupName, 0); } catch (NameNotFoundException e) { /* ignore */ } return null; } /** * Get the {@link PermissionInfo infos} for all permission infos belonging to a group. * * @param groupName the group * @param context the {@code Context} to retrieve {@code PackageManager} * * @return The infos of permissions belonging to the group or null if the group does not have * runtime permissions. */ public static @Nullable List getGroupPermissionInfos(@NonNull String groupName, @NonNull Context context) { try { return Utils.getPermissionInfosForGroup(context.getPackageManager(), groupName); } catch (NameNotFoundException e) { /* ignore */ } try { PermissionInfo permissionInfo = context.getPackageManager() .getPermissionInfo(groupName, 0); List permissions = new ArrayList<>(); permissions.add(permissionInfo); return permissions; } catch (NameNotFoundException e) { /* ignore */ } return null; } /** * Get the label for an application, truncating if it is too long. * * @param applicationInfo the {@link ApplicationInfo} of the application * @param context the {@code Context} to retrieve {@code PackageManager} * * @return the label for the application */ @NonNull public static String getAppLabel(@NonNull ApplicationInfo applicationInfo, @NonNull Context context) { return getAppLabel(applicationInfo, DEFAULT_MAX_LABEL_SIZE_PX, context); } /** * Get the full label for an application without truncation. * * @param applicationInfo the {@link ApplicationInfo} of the application * @param context the {@code Context} to retrieve {@code PackageManager} * * @return the label for the application */ @NonNull public static String getFullAppLabel(@NonNull ApplicationInfo applicationInfo, @NonNull Context context) { return getAppLabel(applicationInfo, 0, context); } /** * Get the label for an application with the ability to control truncating. * * @param applicationInfo the {@link ApplicationInfo} of the application * @param ellipsizeDip see {@link TextUtils#makeSafeForPresentation}. * @param context the {@code Context} to retrieve {@code PackageManager} * * @return the label for the application */ @NonNull private static String getAppLabel(@NonNull ApplicationInfo applicationInfo, float ellipsizeDip, @NonNull Context context) { return BidiFormatter.getInstance().unicodeWrap(applicationInfo.loadSafeLabel( context.getPackageManager(), ellipsizeDip, TextUtils.SAFE_STRING_FLAG_TRIM | TextUtils.SAFE_STRING_FLAG_FIRST_LINE) .toString()); } public static Drawable loadDrawable(PackageManager pm, String pkg, int resId) { try { return pm.getResourcesForApplication(pkg).getDrawable(resId, null); } catch (Resources.NotFoundException | PackageManager.NameNotFoundException e) { Log.d(LOG_TAG, "Couldn't get resource", e); return null; } } /** * Should UI show this permission. * *

If the user cannot change the group, it should not be shown. * * @param group The group that might need to be shown to the user * * @return */ public static boolean shouldShowPermission(Context context, AppPermissionGroup group) { if (!group.isGrantingAllowed()) { return false; } final boolean isPlatformPermission = group.getDeclaringPackage().equals(OS_PKG); // Show legacy permissions only if the user chose that. if (isPlatformPermission && !PermissionMapping.isPlatformPermissionGroup(group.getName())) { return false; } return true; } public static Drawable applyTint(Context context, Drawable icon, int attr) { Theme theme = context.getTheme(); TypedValue typedValue = new TypedValue(); theme.resolveAttribute(attr, typedValue, true); icon = icon.mutate(); icon.setTint(context.getColor(typedValue.resourceId)); return icon; } public static Drawable applyTint(Context context, int iconResId, int attr) { return applyTint(context, context.getDrawable(iconResId), attr); } /** * Get the color resource id based on the attribute * * @return Resource id for the color */ @ColorRes public static int getColorResId(Context context, int attr) { Theme theme = context.getTheme(); TypedValue typedValue = new TypedValue(); theme.resolveAttribute(attr, typedValue, true); return typedValue.resourceId; } /** * Is the group or background group user sensitive? * * @param group The group that might be user sensitive * * @return {@code true} if the group (or it's subgroup) is user sensitive. */ public static boolean isGroupOrBgGroupUserSensitive(AppPermissionGroup group) { return group.isUserSensitive() || (group.getBackgroundPermissions() != null && group.getBackgroundPermissions().isUserSensitive()); } public static boolean areGroupPermissionsIndividuallyControlled(Context context, String group) { if (!context.getPackageManager().arePermissionsIndividuallyControlled()) { return false; } return Manifest.permission_group.SMS.equals(group) || Manifest.permission_group.PHONE.equals(group) || Manifest.permission_group.CONTACTS.equals(group) || Manifest.permission_group.CALL_LOG.equals(group); } public static boolean isPermissionIndividuallyControlled(Context context, String permission) { if (!context.getPackageManager().arePermissionsIndividuallyControlled()) { return false; } return Manifest.permission.READ_CONTACTS.equals(permission) || Manifest.permission.WRITE_CONTACTS.equals(permission) || Manifest.permission.SEND_SMS.equals(permission) || Manifest.permission.RECEIVE_SMS.equals(permission) || Manifest.permission.READ_SMS.equals(permission) || Manifest.permission.RECEIVE_MMS.equals(permission) || Manifest.permission.CALL_PHONE.equals(permission) || Manifest.permission.READ_CALL_LOG.equals(permission) || Manifest.permission.WRITE_CALL_LOG.equals(permission); } /** * Get the message shown to grant a permission group to an app. * * @param appLabel The label of the app * @param packageName The package name of the app * @param groupName The name of the permission group * @param context A context to resolve resources * @param requestRes The resource id of the grant request message * @return The formatted message to be used as title when granting permissions */ @NonNull public static CharSequence getRequestMessage( @NonNull String appLabel, @NonNull String packageName, @NonNull String groupName, @NonNull Context context, @StringRes int requestRes) { String escapedAppLabel = Html.escapeHtml(appLabel); boolean isIsolatedStorage; try { isIsolatedStorage = !isNonIsolatedStorage(context, packageName); } catch (NameNotFoundException e) { isIsolatedStorage = false; } if (groupName.equals(STORAGE) && isIsolatedStorage) { return Html.fromHtml( String.format( context.getResources().getConfiguration().getLocales().get(0), context.getString(R.string.permgrouprequest_storage_isolated), escapedAppLabel), 0); } else if (requestRes != 0) { return Html.fromHtml(context.getResources().getString(requestRes, escapedAppLabel), 0); } return Html.fromHtml( context.getString( R.string.permission_warning_template, escapedAppLabel, loadGroupDescription(context, groupName, context.getPackageManager())), 0); } /** * Get the message shown to grant a permission group to an app. * * @param appLabel The label of the app * @param packageName The package name of the app * @param groupName The name of the permission group * @param context A context to resolve resources * @param requestRes The resource id of the grant request message * @return The formatted message to be used as title when granting permissions */ @NonNull public static CharSequence getRequestMessage( @NonNull String appLabel, @NonNull String packageName, @NonNull String groupName, @NonNull String deviceLabel, @NonNull Context context, Boolean isDeviceAwareMessage, @StringRes int requestRes) { if (!isDeviceAwareMessage) { return getRequestMessage(appLabel, packageName, groupName, context, requestRes); } String escapedAppLabel = Html.escapeHtml(appLabel); boolean isIsolatedStorage; try { isIsolatedStorage = !isNonIsolatedStorage(context, packageName); } catch (NameNotFoundException e) { isIsolatedStorage = false; } if (groupName.equals(STORAGE) && isIsolatedStorage) { String escapedDeviceLabel = Html.escapeHtml(deviceLabel); return Html.fromHtml( String.format( context.getResources().getConfiguration().getLocales().get(0), context.getString( R.string.permgrouprequest_device_aware_storage_isolated), escapedAppLabel, escapedDeviceLabel), 0); } else if (requestRes != 0) { String escapedDeviceLabel = Html.escapeHtml(deviceLabel); return Html.fromHtml(context.getResources().getString(requestRes, escapedAppLabel, escapedDeviceLabel), 0); } return Html.fromHtml( context.getString( R.string.permission_warning_template, escapedAppLabel, loadGroupDescription(context, groupName, context.getPackageManager())), 0); } private static CharSequence loadGroupDescription(Context context, String groupName, @NonNull PackageManager packageManager) { PackageItemInfo groupInfo = getGroupInfo(groupName, context); CharSequence description = null; if (groupInfo instanceof PermissionGroupInfo) { description = ((PermissionGroupInfo) groupInfo).loadDescription(packageManager); } else if (groupInfo instanceof PermissionInfo) { description = ((PermissionInfo) groupInfo).loadDescription(packageManager); } if (description == null || description.length() <= 0) { description = context.getString(R.string.default_permission_description); } return description; } /** * Whether or not the given package has non-isolated storage permissions * @param context The current context * @param packageName The package name to check * @return True if the package has access to non-isolated storage, false otherwise * @throws NameNotFoundException */ public static boolean isNonIsolatedStorage(@NonNull Context context, @NonNull String packageName) throws NameNotFoundException { PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName, 0); AppOpsManager manager = context.getSystemService(AppOpsManager.class); return packageInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.P || (packageInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.R && manager.unsafeCheckOpNoThrow(OPSTR_LEGACY_STORAGE, packageInfo.applicationInfo.uid, packageInfo.packageName) == MODE_ALLOWED); } /** * Gets whether the STORAGE group should be hidden from the UI for this package. This is true * when the platform is T+, and the package has legacy storage access (i.e., either the package * has a targetSdk less than Q, or has a targetSdk equal to Q and has OPSTR_LEGACY_STORAGE). * * TODO jaysullivan: This is always calling AppOpsManager; not taking advantage of LiveData * * @param pkg The package to check */ public static boolean shouldShowStorage(LightPackageInfo pkg) { if (!SdkLevel.isAtLeastT()) { return true; } int targetSdkVersion = pkg.getTargetSdkVersion(); PermissionControllerApplication app = PermissionControllerApplication.get(); Context context = Utils.getUserContext(app, UserHandle.getUserHandleForUid(pkg.getUid())); AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); if (appOpsManager == null) { return true; } return targetSdkVersion < Build.VERSION_CODES.Q || (targetSdkVersion == Build.VERSION_CODES.Q && appOpsManager.unsafeCheckOpNoThrow(OPSTR_LEGACY_STORAGE, pkg.getUid(), pkg.getPackageName()) == MODE_ALLOWED); } /** * Build a string representing the given time if it happened on the current day and the date * otherwise. * * @param context the context. * @param lastAccessTime the time in milliseconds. * * @return a string representing the time or date of the given time or null if the time is 0. */ public static @Nullable String getAbsoluteTimeString(@NonNull Context context, long lastAccessTime) { if (lastAccessTime == 0) { return null; } if (isToday(lastAccessTime)) { return DateFormat.getTimeFormat(context).format(lastAccessTime); } else { return DateFormat.getMediumDateFormat(context).format(lastAccessTime); } } /** * Check whether the given time (in milliseconds) is in the current day. * * @param time the time in milliseconds * * @return whether the given time is in the current day. */ private static boolean isToday(long time) { Calendar today = Calendar.getInstance(Locale.getDefault()); today.setTimeInMillis(System.currentTimeMillis()); today.set(Calendar.HOUR_OF_DAY, 0); today.set(Calendar.MINUTE, 0); today.set(Calendar.SECOND, 0); today.set(Calendar.MILLISECOND, 0); Calendar date = Calendar.getInstance(Locale.getDefault()); date.setTimeInMillis(time); return !date.before(today); } /** * Add a menu item for searching Settings, if there is an activity handling the action. * * @param menu the menu to add the menu item into * @param context the context for checking whether there is an activity handling the action */ public static void prepareSearchMenuItem(@NonNull Menu menu, @NonNull Context context) { Intent intent = new Intent(Settings.ACTION_APP_SEARCH_SETTINGS); if (context.getPackageManager().resolveActivity(intent, 0) == null) { return; } MenuItem searchItem = menu.add(Menu.NONE, Menu.NONE, Menu.NONE, com.android.settingslib.search.widget.R.string.search_menu); searchItem.setIcon(com.android.settingslib.search.widget.R.drawable.ic_search_24dp); searchItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); searchItem.setOnMenuItemClickListener(item -> { try { context.startActivity(intent); } catch (ActivityNotFoundException e) { Log.e(LOG_TAG, "Cannot start activity to search settings", e); } return true; }); } /** * Get badged app icon if necessary, similar as used in the Settings UI. * * @param context The context to use * @param appInfo The app the icon belong to * * @return The icon to use */ public static @NonNull Drawable getBadgedIcon(@NonNull Context context, @NonNull ApplicationInfo appInfo) { UserHandle user = UserHandle.getUserHandleForUid(appInfo.uid); try (IconFactory iconFactory = IconFactory.obtain(context)) { Bitmap iconBmp = iconFactory.createBadgedIconBitmap( appInfo.loadUnbadgedIcon(context.getPackageManager()), user, false).icon; return new BitmapDrawable(context.getResources(), iconBmp); } } /** * Get a string saying what apps with the given permission group can do. * * @param context The context to use * @param groupName The name of the permission group * @param description The description of the permission group * * @return a string saying what apps with the given permission group can do. */ public static @NonNull String getPermissionGroupDescriptionString(@NonNull Context context, @NonNull String groupName, @NonNull CharSequence description) { switch (groupName) { case ACTIVITY_RECOGNITION: return context.getString( R.string.permission_description_summary_activity_recognition); case CALENDAR: return context.getString(R.string.permission_description_summary_calendar); case CALL_LOG: return context.getString(R.string.permission_description_summary_call_log); case CAMERA: return context.getString(R.string.permission_description_summary_camera); case CONTACTS: return context.getString(R.string.permission_description_summary_contacts); case LOCATION: return context.getString(R.string.permission_description_summary_location); case MICROPHONE: return context.getString(R.string.permission_description_summary_microphone); case NEARBY_DEVICES: return context.getString(R.string.permission_description_summary_nearby_devices); case PHONE: return context.getString(R.string.permission_description_summary_phone); case READ_MEDIA_AURAL: return context.getString(R.string.permission_description_summary_read_media_aural); case READ_MEDIA_VISUAL: return context.getString(R.string.permission_description_summary_read_media_visual); case SENSORS: return context.getString(R.string.permission_description_summary_sensors); case SMS: return context.getString(R.string.permission_description_summary_sms); case STORAGE: return context.getString(R.string.permission_description_summary_storage); default: return context.getString(R.string.permission_description_summary_generic, description); } } /** * Whether we should show health permissions as platform permissions in the various * permission controller UI. */ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "UpsideDownCake") public static boolean isHealthPermissionUiEnabled() { final long token = Binder.clearCallingIdentity(); try { return SdkLevel.isAtLeastU() && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, PROPERTY_HEALTH_PERMISSION_UI_ENABLED, true); } finally { Binder.restoreCallingIdentity(token); } } /** * Returns true if the group name passed is that of the Platform health group. * @param permGroupName name of the group that needs to be checked. */ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static Boolean isHealthPermissionGroup(String permGroupName) { return SdkLevel.isAtLeastU() && HEALTH_PERMISSION_GROUP.equals(permGroupName); } /** * Return whether health permission setting entry should be shown or not * * Should not show Health permissions preference if the package doesn't handle * VIEW_PERMISSION_USAGE_INTENT. * * Will show if above is true AND permission is already granted. * * @param packageInfo the {@link PackageInfo} app which uses the permission * @param permGroupName the health permission group name to show * @return {@code TRUE} iff health permission should be shown */ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static Boolean shouldShowHealthPermission(LightPackageInfo packageInfo, String permGroupName) { if (!isHealthPermissionGroup(permGroupName)) { return false; } PermissionControllerApplication app = PermissionControllerApplication.get(); PackageManager pm = app.getPackageManager(); Context context = getUserContext(app, UserHandle.getUserHandleForUid(packageInfo.getUid())); List permissions = new ArrayList<>(); try { permissions.addAll(getPermissionInfosForGroup(pm, permGroupName)); } catch (NameNotFoundException e) { Log.e(LOG_TAG, "No permissions found for permission group " + permGroupName); return false; } // Check in permission is already granted as we should not hide it in the UX at that point. List grantedPermissions = packageInfo.getGrantedPermissions(); for (PermissionInfo permission : permissions) { boolean isCurrentlyGranted = grantedPermissions.contains(permission.name); if (isCurrentlyGranted) { Log.d(LOG_TAG, "At least one Health permission group permission is granted, " + "show permission group entry"); return true; } } Intent viewUsageIntent = new Intent(Intent.ACTION_VIEW_PERMISSION_USAGE); viewUsageIntent.addCategory(HealthConnectManager.CATEGORY_HEALTH_PERMISSIONS); viewUsageIntent.setPackage(packageInfo.getPackageName()); ResolveInfo resolveInfo = pm.resolveActivity(viewUsageIntent, PackageManager.MATCH_ALL); if (resolveInfo == null) { Log.e(LOG_TAG, "Package that asks for Health permission must also handle " + "VIEW_PERMISSION_USAGE_INTENT."); return false; } return true; } /** * Get a device protected storage based shared preferences. Avoid storing sensitive data in it. * * @param context the context to get the shared preferences * @return a device protected storage based shared preferences */ @NonNull public static SharedPreferences getDeviceProtectedSharedPreferences(@NonNull Context context) { if (!context.isDeviceProtectedStorage()) { context = context.createDeviceProtectedStorageContext(); } return context.getSharedPreferences(Constants.PREFERENCES_FILE, MODE_PRIVATE); } public static long getOneTimePermissionsTimeout() { return DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, PROPERTY_ONE_TIME_PERMISSIONS_TIMEOUT_MILLIS, ONE_TIME_PERMISSIONS_TIMEOUT_MILLIS); } /** * Returns the delay in milliseconds before revoking permissions at the end of a one-time * permission session if all processes have been killed. * If the session was triggered by a self-revocation, then revocation should happen * immediately. For a regular one-time permission session, a grace period allows a quick * app restart without losing the permission. * @param isSelfRevoked If true, return the delay for a self-revocation session. Otherwise, * return delay for a regular one-time permission session. */ public static long getOneTimePermissionsKilledDelay(boolean isSelfRevoked) { if (isSelfRevoked) { // For a self-revoked session, we revoke immediately when the process dies. return 0; } return DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, PROPERTY_ONE_TIME_PERMISSIONS_KILLED_DELAY_MILLIS, ONE_TIME_PERMISSIONS_KILLED_DELAY_MILLIS); } /** * Get context of the parent user of the profile group (i.e. usually the 'personal' profile, * not the 'work' profile). * * @param context The context of a user of the profile user group. * * @return The context of the parent user */ public static Context getParentUserContext(@NonNull Context context) { UserHandle parentUser = getSystemServiceSafe(context, UserManager.class) .getProfileParent(UserHandle.of(myUserId())); if (parentUser == null) { return context; } // In a multi profile environment perform all operations as the parent user of the // current profile try { return context.createPackageContextAsUser(context.getPackageName(), 0, parentUser); } catch (PackageManager.NameNotFoundException e) { // cannot happen throw new IllegalStateException("Could not switch to parent user " + parentUser, e); } } /** * The resource id for the request message for a permission group * @param groupName Permission group name * @return The id or 0 if the permission group doesn't exist or have a message */ public static int getRequest(String groupName) { return getRequest(groupName, false); } /** * The resource id for the request message for a permission group for a specific device * * @param groupName Permission group name * @return The id or 0 if the permission group doesn't exist or have a message */ public static int getRequest(String groupName, Boolean isDeviceAwareMessage) { if (isDeviceAwareMessage) { return PERM_GROUP_REQUEST_DEVICE_AWARE_RES.getOrDefault(groupName, 0); } else { return PERM_GROUP_REQUEST_RES.getOrDefault(groupName, 0); } } /** * The resource id for the request detail message for a permission group * @param groupName Permission group name * @return The id or 0 if the permission group doesn't exist or have a message */ public static int getRequestDetail(String groupName) { return PERM_GROUP_REQUEST_DETAIL_RES.getOrDefault(groupName, 0); } /** * The resource id for the background request message for a permission group * @param groupName Permission group name * @return The id or 0 if the permission group doesn't exist or have a message */ public static int getBackgroundRequest(String groupName) { return getBackgroundRequest(groupName, false); } /** * The resource id for the background request message for a permission group for a specific * device * * @param groupName Permission group name * @return The id or 0 if the permission group doesn't exist or have a message */ public static int getBackgroundRequest(String groupName, Boolean isDeviceAwareMessage) { if (isDeviceAwareMessage) { return PERM_GROUP_BACKGROUND_REQUEST_DEVICE_AWARE_RES.getOrDefault(groupName, 0); } else { return PERM_GROUP_BACKGROUND_REQUEST_RES.getOrDefault(groupName, 0); } } /** * The resource id for the background request detail message for a permission group * @param groupName Permission group name * @return The id or 0 if the permission group doesn't exist or have a message */ public static int getBackgroundRequestDetail(String groupName) { return PERM_GROUP_BACKGROUND_REQUEST_DETAIL_RES.getOrDefault(groupName, 0); } /** * The resource id for the upgrade request message for a permission group * @param groupName Permission group name * @return The id or 0 if the permission group doesn't exist or have a message */ public static int getUpgradeRequest(String groupName) { return getUpgradeRequest(groupName, false); } /** * The resource id for the upgrade request message for a permission group for a specific device. * * @param groupName Permission group name * @return The id or 0 if the permission group doesn't exist or have a message */ public static int getUpgradeRequest(String groupName, Boolean isDeviceAwareMessage) { if (isDeviceAwareMessage) { return PERM_GROUP_UPGRADE_REQUEST_DEVICE_AWARE_RES.getOrDefault(groupName, 0); } else { return PERM_GROUP_UPGRADE_REQUEST_RES.getOrDefault(groupName, 0); } } /** * The resource id for the upgrade request detail message for a permission group * @param groupName Permission group name * @return The id or 0 if the permission group doesn't exist or have a message */ public static int getUpgradeRequestDetail(String groupName) { return PERM_GROUP_UPGRADE_REQUEST_DETAIL_RES.getOrDefault(groupName, 0); } /** * The resource id for the fine location request message for a specific device * * @return The id */ public static int getFineLocationRequest(Boolean isDeviceAwareMessage) { if (isDeviceAwareMessage) { return R.string.permgrouprequest_device_aware_fineupgrade; } else { return R.string.permgrouprequest_fineupgrade; } } /** * The resource id for the coarse location request message for a specific device * * @return The id */ public static int getCoarseLocationRequest(Boolean isDeviceAwareMessage) { if (isDeviceAwareMessage) { return R.string.permgrouprequest_device_aware_coarselocation; } else { return R.string.permgrouprequest_coarselocation; } } /** * The resource id for the get more photos request message for a specific device * * @return The id */ public static int getMorePhotosRequest(Boolean isDeviceAwareMessage) { if (isDeviceAwareMessage) { return R.string.permgrouprequest_device_aware_more_photos; } else { return R.string.permgrouprequest_more_photos; } } /** * Returns a random session ID value that's guaranteed to not be {@code INVALID_SESSION_ID}. * * @return A valid session ID. */ public static long getValidSessionId() { long sessionId = INVALID_SESSION_ID; while (sessionId == INVALID_SESSION_ID) { sessionId = new Random().nextLong(); } return sessionId; } /** * Retrieves an existing session ID from the given intent or generates a new one if none is * present. * * @return A valid session ID. */ public static long getOrGenerateSessionId(Intent intent) { long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID); if (sessionId == INVALID_SESSION_ID) { sessionId = getValidSessionId(); } return sessionId; } /** * Gets the label of the Settings application * * @param pm The packageManager used to get the activity resolution * * @return The CharSequence title of the settings app */ @Nullable public static CharSequence getSettingsLabelForNotifications(PackageManager pm) { // We pretend we're the Settings app sending the notification, so figure out its name. Intent openSettingsIntent = new Intent(Settings.ACTION_SETTINGS); ResolveInfo resolveInfo = pm.resolveActivity(openSettingsIntent, MATCH_SYSTEM_ONLY); if (resolveInfo == null) { return null; } return pm.getApplicationLabel(resolveInfo.activityInfo.applicationInfo); } /** * Determines if a given user is disabled, or is a work profile. * @param user The user to check * @return true if the user is disabled, or the user is a work profile */ public static boolean isUserDisabledOrWorkProfile(UserHandle user) { Application app = PermissionControllerApplication.get(); UserManager userManager = app.getSystemService(UserManager.class); // In android TV, parental control accounts are managed profiles return !userManager.getEnabledProfiles().contains(user) || (userManager.isManagedProfile(user.getIdentifier()) && !DeviceUtils.isTelevision(app)); } /** * Determines if a given user ID belongs to a managed profile user. * @param userId The user ID to check * @return true if the user is a managed profile */ public static boolean isUserManagedProfile(int userId) { return PermissionControllerApplication.get() .getSystemService(UserManager.class) .isManagedProfile(userId); } /** * Get all the exempted packages. */ public static Set getExemptedPackages(@NonNull RoleManager roleManager) { Set exemptedPackages = new HashSet<>(); exemptedPackages.add(OS_PKG); for (int i = 0; i < EXEMPTED_ROLES.length; i++) { exemptedPackages.addAll(roleManager.getRoleHolders(EXEMPTED_ROLES[i])); } return exemptedPackages; } /** * Get the timestamp and lastAccessType for the summary text * in app permission groups and permission apps screens * @return Triple with the first being the formatted time * the second being lastAccessType and the third being the formatted date. */ public static Triple getPermissionLastAccessSummaryTimestamp( Long lastAccessTime, Context context, String groupName) { long midnightToday = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).toEpochSecond() * 1000L; long midnightYesterday = ZonedDateTime.now().minusDays(1).truncatedTo(ChronoUnit.DAYS) .toEpochSecond() * 1000L; long yesterdayAtThisTime = ZonedDateTime.now().minusDays(1).toEpochSecond() * 1000L; boolean isLastAccessToday = lastAccessTime != null && midnightToday <= lastAccessTime; boolean isLastAccessWithinPast24h = lastAccessTime != null && yesterdayAtThisTime <= lastAccessTime; boolean isLastAccessTodayOrYesterday = lastAccessTime != null && midnightYesterday <= lastAccessTime; String lastAccessTimeFormatted = ""; String lastAccessDateFormatted = ""; @AppPermsLastAccessType int lastAccessType = NOT_IN_LAST_7D; if (lastAccessTime != null) { lastAccessTimeFormatted = DateFormat.getTimeFormat(context) .format(lastAccessTime); lastAccessDateFormatted = DateFormat.getDateFormat(context) .format(lastAccessTime); if (!PermissionMapping.SENSOR_DATA_PERMISSIONS.contains(groupName)) { // For content providers we show either the last access is within // past 24 hours or past 7 days lastAccessType = isLastAccessWithinPast24h ? LAST_24H_CONTENT_PROVIDER : LAST_7D_CONTENT_PROVIDER; } else { // For sensor data permissions we show if the last access // is today, yesterday or older than yesterday lastAccessType = isLastAccessToday ? LAST_24H_SENSOR_TODAY : isLastAccessTodayOrYesterday ? LAST_24H_SENSOR_YESTERDAY : LAST_7D_SENSOR; } } return new Triple<>(lastAccessTimeFormatted, lastAccessType, lastAccessDateFormatted); } /** * Returns if the permission group is Camera or Microphone (status bar indicators). **/ public static boolean isStatusBarIndicatorPermission(@NonNull String permissionGroupName) { return CAMERA.equals(permissionGroupName) || MICROPHONE.equals(permissionGroupName); } /** * Navigate to notification settings for all apps * @param context The current Context */ public static void navigateToNotificationSettings(@NonNull Context context) { Intent notificationIntent = new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS); context.startActivity(notificationIntent); } /** * Navigate to notification settings for an app * @param context The current Context * @param packageName The package to navigate to * @param user Specifies the user of the package which should be navigated to. If null, the * current user is used. */ public static void navigateToAppNotificationSettings(@NonNull Context context, @NonNull String packageName, @NonNull UserHandle user) { Intent notificationIntent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); notificationIntent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName); context.startActivityAsUser(notificationIntent, user); } /** * Navigate to health connect settings for all apps * @param context The current Context */ public static void navigateToHealthConnectSettings(@NonNull Context context) { Intent healthConnectIntent = new Intent(ACTION_MANAGE_HEALTH_PERMISSIONS); context.startActivity(healthConnectIntent); } /** * Navigate to health connect settings for an app * @param context The current Context * @param packageName The package's health connect settings to navigate to */ public static void navigateToAppHealthConnectSettings(@NonNull Context context, @NonNull String packageName, @NonNull UserHandle user) { Intent appHealthConnectIntent = new Intent(ACTION_MANAGE_HEALTH_PERMISSIONS); appHealthConnectIntent.putExtra(EXTRA_PACKAGE_NAME, packageName); appHealthConnectIntent.putExtra(Intent.EXTRA_USER, user); context.startActivity(appHealthConnectIntent); } /** * Returns if a card should be shown if the sensor is blocked **/ public static boolean shouldDisplayCardIfBlocked(@NonNull String permissionGroupName) { return CAMERA.equals(permissionGroupName) || MICROPHONE.equals(permissionGroupName) || LOCATION.equals(permissionGroupName); } /** * Returns the sensor code for a permission **/ @RequiresApi(Build.VERSION_CODES.S) public static int getSensorCode(@NonNull String permissionGroupName) { return PERM_SENSOR_CODES.getOrDefault(permissionGroupName, -1); } /** * Returns the blocked icon code for a permission **/ public static int getBlockedIcon(@NonNull String permissionGroupName) { return PERM_BLOCKED_ICON.getOrDefault(permissionGroupName, -1); } /** * Returns the blocked title code for a permission **/ public static int getBlockedTitle(@NonNull String permissionGroupName) { return PERM_BLOCKED_TITLE.getOrDefault(permissionGroupName, -1); } /** * Returns the blocked title code on automotive for a permission **/ public static int getBlockedTitleAutomotive(@NonNull String permissionGroupName) { return PERM_BLOCKED_TITLE_AUTOMOTIVE.getOrDefault(permissionGroupName, -1); } /** * Returns if the permission group has a background mode, even if the background mode is * introduced in a platform version after the one currently running **/ public static boolean hasPermWithBackgroundModeCompat(LightAppPermGroup group) { if (SdkLevel.isAtLeastS()) { return group.getHasPermWithBackgroundMode(); } String groupName = group.getPermGroupName(); return group.getHasPermWithBackgroundMode() || Manifest.permission_group.CAMERA.equals(groupName) || Manifest.permission_group.MICROPHONE.equals(groupName); } /** * Returns the appropriate enterprise string for the provided IDs */ @NonNull public static String getEnterpriseString(@NonNull Context context, @NonNull String updatableStringId, int defaultStringId, @NonNull Object... formatArgs) { return SdkLevel.isAtLeastT() ? getUpdatableEnterpriseString( context, updatableStringId, defaultStringId, formatArgs) : context.getString(defaultStringId, formatArgs); } @RequiresApi(Build.VERSION_CODES.TIRAMISU) @NonNull private static String getUpdatableEnterpriseString(@NonNull Context context, @NonNull String updatableStringId, int defaultStringId, @NonNull Object... formatArgs) { DevicePolicyManager dpm = getSystemServiceSafe(context, DevicePolicyManager.class); return dpm.getResources().getString(updatableStringId, () -> context.getString( defaultStringId, formatArgs), formatArgs); } /** * Get {@link PackageInfo} for this ComponentName. * * @param context The current Context * @param component component to get package info for * @return The package info * * @throws PackageManager.NameNotFoundException if package does not exist */ @NonNull public static PackageInfo getPackageInfoForComponentName(@NonNull Context context, @NonNull ComponentName component) throws PackageManager.NameNotFoundException { return context.getPackageManager().getPackageInfo(component.getPackageName(), 0); } /** * Return the label to use for this application. * * @param context The current Context * @param applicationInfo The {@link ApplicationInfo} of the application to get the label of. * @return the label associated with this application, or its name if there is no label. */ @NonNull public static String getApplicationLabel(@NonNull Context context, @NonNull ApplicationInfo applicationInfo) { return context.getPackageManager().getApplicationLabel(applicationInfo).toString(); } /** * Returns whether the given user should be shown in the Settings UI in SdkLevel V+. This method * will always return true for SdkLevels below V. * * @param userHandle The user for which to check whether it should be shown or not. * @return true if it should be shown, false otherwise. */ public static boolean shouldShowInSettings(UserHandle userHandle, UserManager userManager) { return !SdkLevel.isAtLeastV() || shouldShowInSettingsInternal(userHandle, userManager); } /** * Returns whether the given user should be shown in the Settings UI. * * @param userHandle The user for which to check whether it should be shown or not. * @return true if it should be shown, false otherwise. */ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.VANILLA_ICE_CREAM) private static boolean shouldShowInSettingsInternal( UserHandle userHandle, UserManager userManager) { var userProperties = userManager.getUserProperties(userHandle); return !userManager.isQuietModeEnabled(userHandle) || userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; } /** * Check whether an application is restricted for this setting identifier and return the * {@code Intent} for the restriction if it is. * * @param user the user to check for * @param context the {@code Context} to retrieve system services * * @return the {@code Intent} for the restriction if the application is restricted for this * setting identifier, or {@code null} otherwise. */ @Nullable public static Intent getApplicationEnhancedConfirmationRestrictedIntentAsUser( @NonNull UserHandle user, @NonNull Context context, @Nullable String packageName, @Nullable String settingIdentifier) { if (SdkLevel.isAtLeastV() && Flags.enhancedConfirmationModeApisEnabled()) { Context userContext = Utils.getUserContext(context, user); EnhancedConfirmationManager userEnhancedConfirmationManager = userContext.getSystemService(EnhancedConfirmationManager.class); if (packageName == null || settingIdentifier == null) return null; try { boolean isRestricted = userEnhancedConfirmationManager.isRestricted(packageName, settingIdentifier); if (isRestricted) { return userEnhancedConfirmationManager.createRestrictedSettingDialogIntent( packageName, settingIdentifier); } } catch (PackageManager.NameNotFoundException e) { Log.w(LOG_TAG, "Cannot check enhanced confirmation restriction for package: " + packageName, e); } } return null; } }