blob: e4caec32a189db376ecdc1f1a9e149f6159af6bf [file] [log] [blame]
/*
* 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.packageinstaller.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.MEDIA_AURAL;
import static android.Manifest.permission_group.MEDIA_VISUAL;
import static android.Manifest.permission_group.MICROPHONE;
import static android.Manifest.permission_group.PHONE;
import static android.Manifest.permission_group.SENSORS;
import static android.Manifest.permission_group.SMS;
import static android.Manifest.permission_group.STORAGE;
import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.graphics.drawable.Drawable;
import android.os.Parcelable;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.Html;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.text.BidiFormatter;
import androidx.core.util.Preconditions;
import com.android.packageinstaller.permission.model.AppPermissionGroup;
import com.android.packageinstaller.permission.model.AppPermissions;
import com.android.packageinstaller.permission.model.PermissionApps.PermissionApp;
import com.android.permissioncontroller.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class Utils {
private static final String LOG_TAG = "Utils";
public static final String OS_PKG = "android";
public static final float DEFAULT_MAX_LABEL_SIZE_PX = 500f;
/** Mapping permission -> group for all dangerous platform permissions */
private static final ArrayMap<String, String> PLATFORM_PERMISSIONS;
/** Mapping group -> permissions for all dangerous platform permissions */
private static final ArrayMap<String, ArrayList<String>> PLATFORM_PERMISSION_GROUPS;
private static final Intent LAUNCHER_INTENT = new Intent(Intent.ACTION_MAIN, null)
.addCategory(Intent.CATEGORY_LAUNCHER);
static {
PLATFORM_PERMISSIONS = new ArrayMap<>();
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_CONTACTS, CONTACTS);
PLATFORM_PERMISSIONS.put(Manifest.permission.WRITE_CONTACTS, CONTACTS);
PLATFORM_PERMISSIONS.put(Manifest.permission.GET_ACCOUNTS, CONTACTS);
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_CALENDAR, CALENDAR);
PLATFORM_PERMISSIONS.put(Manifest.permission.WRITE_CALENDAR, CALENDAR);
PLATFORM_PERMISSIONS.put(Manifest.permission.SEND_SMS, SMS);
PLATFORM_PERMISSIONS.put(Manifest.permission.RECEIVE_SMS, SMS);
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_SMS, SMS);
PLATFORM_PERMISSIONS.put(Manifest.permission.RECEIVE_MMS, SMS);
PLATFORM_PERMISSIONS.put(Manifest.permission.RECEIVE_WAP_PUSH, SMS);
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_CELL_BROADCASTS, SMS);
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_EXTERNAL_STORAGE, STORAGE);
PLATFORM_PERMISSIONS.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, STORAGE);
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_MEDIA_AUDIO, MEDIA_AURAL);
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_MEDIA_IMAGES, MEDIA_VISUAL);
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_MEDIA_VIDEO, MEDIA_VISUAL);
PLATFORM_PERMISSIONS.put(Manifest.permission.ACCESS_MEDIA_LOCATION, MEDIA_VISUAL);
PLATFORM_PERMISSIONS.put(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION);
PLATFORM_PERMISSIONS.put(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION);
PLATFORM_PERMISSIONS.put(Manifest.permission.ACCESS_BACKGROUND_LOCATION, LOCATION);
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_CALL_LOG, CALL_LOG);
PLATFORM_PERMISSIONS.put(Manifest.permission.WRITE_CALL_LOG, CALL_LOG);
PLATFORM_PERMISSIONS.put(Manifest.permission.PROCESS_OUTGOING_CALLS, CALL_LOG);
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_PHONE_STATE, PHONE);
PLATFORM_PERMISSIONS.put(Manifest.permission.READ_PHONE_NUMBERS, PHONE);
PLATFORM_PERMISSIONS.put(Manifest.permission.CALL_PHONE, PHONE);
PLATFORM_PERMISSIONS.put(Manifest.permission.ADD_VOICEMAIL, PHONE);
PLATFORM_PERMISSIONS.put(Manifest.permission.USE_SIP, PHONE);
PLATFORM_PERMISSIONS.put(Manifest.permission.ANSWER_PHONE_CALLS, PHONE);
PLATFORM_PERMISSIONS.put(Manifest.permission.ACCEPT_HANDOVER, PHONE);
PLATFORM_PERMISSIONS.put(Manifest.permission.RECORD_AUDIO, MICROPHONE);
PLATFORM_PERMISSIONS.put(Manifest.permission.ACTIVITY_RECOGNITION, ACTIVITY_RECOGNITION);
PLATFORM_PERMISSIONS.put(Manifest.permission.CAMERA, CAMERA);
PLATFORM_PERMISSIONS.put(Manifest.permission.BODY_SENSORS, SENSORS);
PLATFORM_PERMISSION_GROUPS = new ArrayMap<>();
int numPlatformPermissions = PLATFORM_PERMISSIONS.size();
for (int i = 0; i < numPlatformPermissions; i++) {
String permission = PLATFORM_PERMISSIONS.keyAt(i);
String permissionGroup = PLATFORM_PERMISSIONS.valueAt(i);
ArrayList<String> permissionsOfThisGroup = PLATFORM_PERMISSION_GROUPS.get(
permissionGroup);
if (permissionsOfThisGroup == null) {
permissionsOfThisGroup = new ArrayList<>();
PLATFORM_PERMISSION_GROUPS.put(permissionGroup, permissionsOfThisGroup);
}
permissionsOfThisGroup.add(permission);
}
}
private Utils() {
/* do nothing - hide constructor */
}
/**
* {@code @NonNull} version of {@link Context#getSystemService(Class)}
*/
public static @NonNull <M> M getSystemServiceSafe(@NonNull Context context, Class<M> clazz) {
return Preconditions.checkNotNull(context.getSystemService(clazz),
"Could not resolve " + clazz.getSimpleName());
}
/**
* {@code @NonNull} version of {@link Context#getSystemService(Class)}
*/
public static @NonNull <M> M getSystemServiceSafe(@NonNull Context context, Class<M> 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 extends Parcelable> 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);
}
/**
* Get permission group a platform permission belongs to.
*
* @param permission the permission to resolve
*
* @return The group the permission belongs to
*/
private static @Nullable String getGroupOfPlatformPermission(@NonNull String permission) {
return PLATFORM_PERMISSIONS.get(permission);
}
/**
* Get name of the permission group a permission belongs to.
*
* @param permission the {@link PermissionInfo info} of the permission to resolve
*
* @return The group the permission belongs to
*/
public static @Nullable String getGroupOfPermission(@NonNull PermissionInfo permission) {
String groupName = Utils.getGroupOfPlatformPermission(permission.name);
if (groupName == null) {
groupName = permission.group;
}
return groupName;
}
/**
* Get the {@link PermissionInfo infos} for all platform permissions belonging to a group.
*
* @param pm Package manager to use to resolve permission infos
* @param group the group
*
* @return The infos for platform permissions belonging to the group or an empty list if the
* group is not does not have platform runtime permissions
*/
private static @NonNull List<PermissionInfo> getPlatformPermissionsOfGroup(
@NonNull PackageManager pm, @NonNull String group) {
ArrayList<PermissionInfo> permInfos = new ArrayList<>();
ArrayList<String> permissions = PLATFORM_PERMISSION_GROUPS.get(group);
if (permissions == null) {
return Collections.emptyList();
}
int numPermissions = permissions.size();
for (int i = 0; i < numPermissions; i++) {
String permName = permissions.get(i);
PermissionInfo permInfo;
try {
permInfo = pm.getPermissionInfo(permName, 0);
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(permName + " not defined by platform", e);
}
permInfos.add(permInfo);
}
return permInfos;
}
/**
* 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 is not
* does not have runtime permissions
*/
public static @NonNull List<PermissionInfo> getPermissionInfosForGroup(
@NonNull PackageManager pm, @NonNull String group)
throws PackageManager.NameNotFoundException {
List<PermissionInfo> permissions = pm.queryPermissionsByGroup(group, 0);
permissions.addAll(Utils.getPlatformPermissionsOfGroup(pm, group));
return permissions;
}
/**
* Get the label for an application.
*
* @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 BidiFormatter.getInstance().unicodeWrap(applicationInfo.loadSafeLabel(
context.getPackageManager(), DEFAULT_MAX_LABEL_SIZE_PX,
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;
}
}
public static boolean isModernPermissionGroup(String name) {
return PLATFORM_PERMISSION_GROUPS.containsKey(name);
}
/**
* Should UI show this permission.
*
* <p>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
&& !Utils.isModernPermissionGroup(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);
}
public static ArraySet<String> getLauncherPackages(Context context) {
ArraySet<String> launcherPkgs = new ArraySet<>();
for (ResolveInfo info :
context.getPackageManager().queryIntentActivities(LAUNCHER_INTENT, 0)) {
launcherPkgs.add(info.activityInfo.packageName);
}
return launcherPkgs;
}
public static List<ApplicationInfo> getAllInstalledApplications(Context context) {
return context.getPackageManager().getInstalledApplications(0);
}
public static boolean isSystem(PermissionApp app, ArraySet<String> launcherPkgs) {
return isSystem(app.getAppInfo(), launcherPkgs);
}
public static boolean isSystem(AppPermissions app, ArraySet<String> launcherPkgs) {
return isSystem(app.getPackageInfo().applicationInfo, launcherPkgs);
}
public static boolean isSystem(ApplicationInfo info, ArraySet<String> launcherPkgs) {
return ((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0)
&& (info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0
&& !launcherPkgs.contains(info.packageName);
}
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);
}
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 group the group to be granted
* @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
*/
public static CharSequence getRequestMessage(CharSequence appLabel, AppPermissionGroup group,
Context context, @StringRes int requestRes) {
if (requestRes != 0) {
try {
return Html.fromHtml(context.getPackageManager().getResourcesForApplication(
group.getDeclaringPackage()).getString(requestRes, appLabel), 0);
} catch (PackageManager.NameNotFoundException ignored) {
}
}
return Html.fromHtml(context.getString(R.string.permission_warning_template, appLabel,
group.getDescription()), 0);
}
/**
* Build a string representing the number of milliseconds passed in. It rounds to the nearest
* unit. For example, given a duration of 3500 and an English locale, this can return
* "3 seconds".
* @param context The context.
* @param duration The number of milliseconds.
* @return a string representing the given number of milliseconds.
*/
public static @NonNull String getTimeDiffStr(@NonNull Context context, long duration) {
long seconds = Math.max(1, duration / 1000);
if (seconds < 60) {
return context.getResources().getQuantityString(R.plurals.seconds, (int) seconds,
seconds);
}
long minutes = seconds / 60;
if (minutes < 60) {
return context.getResources().getQuantityString(R.plurals.minutes, (int) minutes,
minutes);
}
long hours = minutes / 60;
if (hours < 24) {
return context.getResources().getQuantityString(R.plurals.hours, (int) hours, hours);
}
long days = hours / 24;
return context.getResources().getQuantityString(R.plurals.days, (int) days, days);
}
/**
* 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, R.string.search_menu);
searchItem.setIcon(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;
});
}
}