summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLuca Stefani <luca.stefani.ge1@gmail.com>2019-09-04 17:52:04 +0200
committerMichael Bestas <mkbestas@lineageos.org>2019-09-04 21:13:25 +0300
commitd8d0d6065d513345990ba10ff01a62654b21df32 (patch)
tree51efdb050c856d548d51682db636c4cf05e07abc
parent24705070d6210f5dc08bc5e73f8e181215221eb9 (diff)
downloadandroid_packages_apps_PackageInstaller-d8d0d6065d513345990ba10ff01a62654b21df32.tar.gz
android_packages_apps_PackageInstaller-d8d0d6065d513345990ba10ff01a62654b21df32.tar.bz2
android_packages_apps_PackageInstaller-d8d0d6065d513345990ba10ff01a62654b21df32.zip
Revert "Remove Permissions Hub."
This reverts commit 5747740517e37ebe05209fde129cc776af4c2127. Change-Id: Ic38e77e8b11baa2dded3593089720d6592d8ddb6
-rw-r--r--src/com/android/packageinstaller/permission/model/PermissionUsages.java184
-rw-r--r--src/com/android/packageinstaller/permission/service/PermissionControllerServiceImpl.java50
-rw-r--r--src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java34
-rw-r--r--src/com/android/packageinstaller/permission/ui/ReviewOngoingUsageActivity.java45
-rw-r--r--src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionsFragment.java34
-rw-r--r--src/com/android/packageinstaller/permission/ui/auto/AutoPermissionAppsFragment.java34
-rw-r--r--src/com/android/packageinstaller/permission/ui/auto/ReviewOngoingUsageAutoFragment.java45
-rw-r--r--src/com/android/packageinstaller/permission/ui/handheld/AppPermissionFragment.java103
-rw-r--r--src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java30
-rw-r--r--src/com/android/packageinstaller/permission/ui/handheld/ExpandablePreferenceGroup.java135
-rw-r--r--src/com/android/packageinstaller/permission/ui/handheld/PermissionAppsFragment.java47
-rw-r--r--src/com/android/packageinstaller/permission/ui/handheld/PermissionControlPreference.java17
-rw-r--r--src/com/android/packageinstaller/permission/ui/handheld/PermissionUsageFragment.java1056
-rw-r--r--src/com/android/packageinstaller/permission/ui/handheld/ReviewOngoingUsageFragment.java251
-rw-r--r--src/com/android/packageinstaller/permission/utils/Utils.java72
15 files changed, 2127 insertions, 10 deletions
diff --git a/src/com/android/packageinstaller/permission/model/PermissionUsages.java b/src/com/android/packageinstaller/permission/model/PermissionUsages.java
index 32db2077..0172635f 100644
--- a/src/com/android/packageinstaller/permission/model/PermissionUsages.java
+++ b/src/com/android/packageinstaller/permission/model/PermissionUsages.java
@@ -16,6 +16,12 @@
package com.android.packageinstaller.permission.model;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.HistoricalOps;
+import android.app.AppOpsManager.HistoricalOpsRequest;
+import android.app.AppOpsManager.HistoricalPackageOps;
+import android.app.AppOpsManager.HistoricalUidOps;
+import android.app.AppOpsManager.PackageOps;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.AsyncTaskLoader;
@@ -23,13 +29,23 @@ import android.content.Context;
import android.content.Loader;
import android.os.Bundle;
import android.os.Process;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.packageinstaller.permission.model.AppPermissionUsage.Builder;
+import com.android.packageinstaller.permission.model.PermissionApps.PermissionApp;
+import com.android.packageinstaller.permission.utils.Utils;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Loads all permission usages for a set of apps and permission groups.
@@ -126,7 +142,27 @@ public final class PermissionUsages implements LoaderCallbacks<List<AppPermissio
public static @Nullable AppPermissionUsage.GroupUsage loadLastGroupUsage(
@NonNull Context context, @NonNull AppPermissionGroup group) {
- return null;
+ if (!Utils.isPermissionsHubEnabled()) {
+ return null;
+ }
+ final ArraySet<String> opNames = new ArraySet<>();
+ final List<Permission> permissions = group.getPermissions();
+ final int permCount = permissions.size();
+ for (int i = 0; i < permCount; i++) {
+ final Permission permission = permissions.get(i);
+ final String opName = permission.getAppOp();
+ if (opName != null) {
+ opNames.add(opName);
+ }
+ }
+ final String[] opNamesArray = opNames.toArray(new String[opNames.size()]);
+ final List<PackageOps> usageOps = context.getSystemService(AppOpsManager.class)
+ .getOpsForPackage(group.getApp().applicationInfo.uid,
+ group.getApp().packageName, opNamesArray);
+ if (usageOps == null || usageOps.isEmpty()) {
+ return null;
+ }
+ return new AppPermissionUsage.GroupUsage(group, usageOps.get(0), null);
}
private static final class UsageLoader extends AsyncTaskLoader<List<AppPermissionUsage>> {
@@ -158,7 +194,151 @@ public final class PermissionUsages implements LoaderCallbacks<List<AppPermissio
@Override
public @NonNull List<AppPermissionUsage> loadInBackground() {
- return Collections.emptyList();
+ final List<PermissionGroup> groups = PermissionGroups.getPermissionGroups(
+ getContext(), this::isLoadInBackgroundCanceled, mGetUiInfo,
+ mGetNonPlatformPermissions, mFilterPermissionGroups, mFilterPackageName);
+ if (!Utils.isPermissionsHubEnabled()) {
+ return Collections.emptyList();
+ }
+
+ if (groups.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final List<AppPermissionUsage> usages = new ArrayList<>();
+ final ArraySet<String> opNames = new ArraySet<>();
+ final ArrayMap<Pair<Integer, String>, AppPermissionUsage.Builder> usageBuilders =
+ new ArrayMap<>();
+
+ final int groupCount = groups.size();
+ for (int groupIdx = 0; groupIdx < groupCount; groupIdx++) {
+ final PermissionGroup group = groups.get(groupIdx);
+ // Filter out third party permissions
+ if (!group.getDeclaringPackage().equals(Utils.OS_PKG)) {
+ continue;
+ }
+ if (!Utils.shouldShowPermissionUsage(group.getName())) {
+ continue;
+ }
+
+ groups.add(group);
+
+ final List<PermissionApp> permissionApps = group.getPermissionApps().getApps();
+ final int appCount = permissionApps.size();
+ for (int appIdx = 0; appIdx < appCount; appIdx++) {
+ final PermissionApp permissionApp = permissionApps.get(appIdx);
+ if (mFilterUid != Process.INVALID_UID
+ && permissionApp.getAppInfo().uid != mFilterUid) {
+ continue;
+ }
+
+ final AppPermissionGroup appPermGroup = permissionApp.getPermissionGroup();
+ if (!Utils.shouldShowPermission(getContext(), appPermGroup)) {
+ continue;
+ }
+ final Pair<Integer, String> usageKey = Pair.create(permissionApp.getUid(),
+ permissionApp.getPackageName());
+ AppPermissionUsage.Builder usageBuilder = usageBuilders.get(usageKey);
+ if (usageBuilder == null) {
+ usageBuilder = new Builder(permissionApp);
+ usageBuilders.put(usageKey, usageBuilder);
+ }
+ usageBuilder.addGroup(appPermGroup);
+ final List<Permission> permissions = appPermGroup.getPermissions();
+ final int permCount = permissions.size();
+ for (int permIdx = 0; permIdx < permCount; permIdx++) {
+ final Permission permission = permissions.get(permIdx);
+ final String opName = permission.getAppOp();
+ if (opName != null) {
+ opNames.add(opName);
+ }
+ }
+ }
+ }
+
+ if (usageBuilders.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+
+ // Get last usage data and put in a map for a quick lookup.
+ final ArrayMap<Pair<Integer, String>, PackageOps> lastUsages =
+ new ArrayMap<>(usageBuilders.size());
+ final String[] opNamesArray = opNames.toArray(new String[opNames.size()]);
+ if ((mUsageFlags & USAGE_FLAG_LAST) != 0) {
+ final List<PackageOps> usageOps;
+ if (mFilterPackageName != null || mFilterUid != Process.INVALID_UID) {
+ usageOps = appOpsManager.getOpsForPackage(mFilterUid, mFilterPackageName,
+ opNamesArray);
+ } else {
+ usageOps = appOpsManager.getPackagesForOps(opNamesArray);
+ }
+ if (usageOps != null && !usageOps.isEmpty()) {
+ final int usageOpsCount = usageOps.size();
+ for (int i = 0; i < usageOpsCount; i++) {
+ final PackageOps usageOp = usageOps.get(i);
+ lastUsages.put(Pair.create(usageOp.getUid(), usageOp.getPackageName()),
+ usageOp);
+ }
+ }
+ }
+
+ if (isLoadInBackgroundCanceled()) {
+ return Collections.emptyList();
+ }
+
+ // Get historical usage data and put in a map for a quick lookup
+ final ArrayMap<Pair<Integer, String>, HistoricalPackageOps> historicalUsages =
+ new ArrayMap<>(usageBuilders.size());
+ if ((mUsageFlags & USAGE_FLAG_HISTORICAL) != 0) {
+ final AtomicReference<HistoricalOps> historicalOpsRef = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final HistoricalOpsRequest request = new HistoricalOpsRequest.Builder(
+ mFilterBeginTimeMillis, mFilterEndTimeMillis)
+ .setUid(mFilterUid)
+ .setPackageName(mFilterPackageName)
+ .setOpNames(new ArrayList<>(opNames))
+ .setFlags(AppOpsManager.OP_FLAGS_ALL_TRUSTED)
+ .build();
+ appOpsManager.getHistoricalOps(request, Runnable::run,
+ (HistoricalOps ops) -> {
+ historicalOpsRef.set(ops);
+ latch.countDown();
+ });
+ try {
+ latch.await(5, TimeUnit.DAYS);
+ } catch (InterruptedException ignored) {}
+
+ final HistoricalOps historicalOps = historicalOpsRef.get();
+ if (historicalOps != null) {
+ final int uidCount = historicalOps.getUidCount();
+ for (int i = 0; i < uidCount; i++) {
+ final HistoricalUidOps uidOps = historicalOps.getUidOpsAt(i);
+ final int packageCount = uidOps.getPackageCount();
+ for (int j = 0; j < packageCount; j++) {
+ final HistoricalPackageOps packageOps = uidOps.getPackageOpsAt(j);
+ historicalUsages.put(
+ Pair.create(uidOps.getUid(), packageOps.getPackageName()),
+ packageOps);
+ }
+ }
+ }
+ }
+
+ // Construct the historical usages based on data we fetched
+ final int builderCount = usageBuilders.size();
+ for (int i = 0; i < builderCount; i++) {
+ final Pair<Integer, String> key = usageBuilders.keyAt(i);
+ final Builder usageBuilder = usageBuilders.valueAt(i);
+ final PackageOps lastUsage = lastUsages.get(key);
+ usageBuilder.setLastUsage(lastUsage);
+ final HistoricalPackageOps historicalUsage = historicalUsages.get(key);
+ usageBuilder.setHistoricalUsage(historicalUsage);
+ usages.add(usageBuilder.build());
+ }
+
+ return usages;
}
}
}
diff --git a/src/com/android/packageinstaller/permission/service/PermissionControllerServiceImpl.java b/src/com/android/packageinstaller/permission/service/PermissionControllerServiceImpl.java
index 6427ba36..178ac662 100644
--- a/src/com/android/packageinstaller/permission/service/PermissionControllerServiceImpl.java
+++ b/src/com/android/packageinstaller/permission/service/PermissionControllerServiceImpl.java
@@ -47,8 +47,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.packageinstaller.permission.model.AppPermissionGroup;
+import com.android.packageinstaller.permission.model.AppPermissionUsage;
+import com.android.packageinstaller.permission.model.AppPermissionUsage.GroupUsage;
import com.android.packageinstaller.permission.model.AppPermissions;
import com.android.packageinstaller.permission.model.Permission;
+import com.android.packageinstaller.permission.model.PermissionUsages;
import com.android.packageinstaller.permission.utils.Utils;
import org.xmlpull.v1.XmlPullParser;
@@ -496,7 +499,52 @@ public final class PermissionControllerServiceImpl extends PermissionControllerS
private @NonNull List<RuntimePermissionUsageInfo> onGetPermissionUsages(
boolean countSystem, long numMillis) {
- return Collections.emptyList();
+ ArrayMap<String, Integer> groupUsers = new ArrayMap<>();
+
+ long curTime = System.currentTimeMillis();
+ PermissionUsages usages = new PermissionUsages(this);
+ long filterTimeBeginMillis = Math.max(System.currentTimeMillis() - numMillis, 0);
+ usages.load(null, null, filterTimeBeginMillis, Long.MAX_VALUE,
+ PermissionUsages.USAGE_FLAG_LAST | PermissionUsages.USAGE_FLAG_HISTORICAL, null,
+ false, false, null, true);
+
+ List<AppPermissionUsage> appPermissionUsages = usages.getUsages();
+ int numApps = appPermissionUsages.size();
+ for (int appNum = 0; appNum < numApps; appNum++) {
+ AppPermissionUsage appPermissionUsage = appPermissionUsages.get(appNum);
+
+ List<GroupUsage> appGroups = appPermissionUsage.getGroupUsages();
+ int numGroups = appGroups.size();
+ for (int groupNum = 0; groupNum < numGroups; groupNum++) {
+ GroupUsage groupUsage = appGroups.get(groupNum);
+
+ if (groupUsage.getLastAccessTime() < filterTimeBeginMillis) {
+ continue;
+ }
+ if (!shouldShowPermission(this, groupUsage.getGroup())) {
+ continue;
+ }
+ if (!countSystem && !Utils.isGroupOrBgGroupUserSensitive(groupUsage.getGroup())) {
+ continue;
+ }
+
+ String groupName = groupUsage.getGroup().getName();
+ Integer numUsers = groupUsers.get(groupName);
+ if (numUsers == null) {
+ groupUsers.put(groupName, 1);
+ } else {
+ groupUsers.put(groupName, numUsers + 1);
+ }
+ }
+ }
+
+ List<RuntimePermissionUsageInfo> users = new ArrayList<>();
+ int numGroups = groupUsers.size();
+ for (int groupNum = 0; groupNum < numGroups; groupNum++) {
+ users.add(new RuntimePermissionUsageInfo(groupUsers.keyAt(groupNum),
+ groupUsers.valueAt(groupNum)));
+ }
+ return users;
}
@Override
diff --git a/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java b/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java
index 98c26d1c..9ec35ea5 100644
--- a/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java
+++ b/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java
@@ -36,7 +36,9 @@ import com.android.packageinstaller.permission.ui.auto.AutoAppPermissionsFragmen
import com.android.packageinstaller.permission.ui.auto.AutoManageStandardPermissionsFragment;
import com.android.packageinstaller.permission.ui.auto.AutoPermissionAppsFragment;
import com.android.packageinstaller.permission.ui.handheld.ManageStandardPermissionsFragment;
+import com.android.packageinstaller.permission.ui.handheld.PermissionUsageFragment;
import com.android.packageinstaller.permission.ui.wear.AppPermissionsFragmentWear;
+import com.android.packageinstaller.permission.utils.Utils;
import com.android.permissioncontroller.R;
import java.util.Random;
@@ -86,9 +88,35 @@ public final class ManagePermissionsActivity extends FragmentActivity {
}
break;
- case Intent.ACTION_REVIEW_PERMISSION_USAGE:
- finish();
- return;
+ case Intent.ACTION_REVIEW_PERMISSION_USAGE: {
+ if (!Utils.isPermissionsHubEnabled()) {
+ finish();
+ return;
+ }
+
+ permissionName = getIntent().getStringExtra(Intent.EXTRA_PERMISSION_NAME);
+ String groupName = getIntent().getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME);
+ long numMillis = getIntent().getLongExtra(Intent.EXTRA_DURATION_MILLIS,
+ Long.MAX_VALUE);
+
+ if (permissionName != null) {
+ String permGroupName = Utils.getGroupOfPlatformPermission(permissionName);
+ if (permGroupName == null) {
+ Log.w(LOG_TAG, "Invalid platform permission: " + permissionName);
+ }
+ if (groupName != null && !groupName.equals(permGroupName)) {
+ Log.i(LOG_TAG,
+ "Inconsistent EXTRA_PERMISSION_NAME / EXTRA_PERMISSION_GROUP_NAME");
+ finish();
+ return;
+ }
+ if (groupName == null) {
+ groupName = permGroupName;
+ }
+ }
+
+ androidXFragment = PermissionUsageFragment.newInstance(groupName, numMillis);
+ } break;
case Intent.ACTION_MANAGE_APP_PERMISSIONS: {
String packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
diff --git a/src/com/android/packageinstaller/permission/ui/ReviewOngoingUsageActivity.java b/src/com/android/packageinstaller/permission/ui/ReviewOngoingUsageActivity.java
index f81c1d1b..5568216e 100644
--- a/src/com/android/packageinstaller/permission/ui/ReviewOngoingUsageActivity.java
+++ b/src/com/android/packageinstaller/permission/ui/ReviewOngoingUsageActivity.java
@@ -16,16 +16,57 @@
package com.android.packageinstaller.permission.ui;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
+import android.content.Intent;
import android.os.Bundle;
+import android.view.MenuItem;
+import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
+import com.android.packageinstaller.DeviceUtils;
+import com.android.packageinstaller.permission.ui.auto.ReviewOngoingUsageAutoFragment;
+import com.android.packageinstaller.permission.ui.handheld.ReviewOngoingUsageFragment;
+
+/**
+ * A dialog listing the currently uses of camera, microphone, and location.
+ */
public final class ReviewOngoingUsageActivity extends FragmentActivity {
+ // Number of milliseconds in the past to look for accesses if nothing was specified.
+ private static final long DEFAULT_MILLIS = 5000;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- finish();
- return;
+
+ getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+
+ long numMillis = getIntent().getLongExtra(Intent.EXTRA_DURATION_MILLIS, DEFAULT_MILLIS);
+ if (DeviceUtils.isAuto(this)) {
+ getSupportFragmentManager().beginTransaction().replace(android.R.id.content,
+ ReviewOngoingUsageAutoFragment.newInstance(numMillis)).commit();
+ } else {
+ getSupportFragmentManager().beginTransaction().replace(android.R.id.content,
+ ReviewOngoingUsageFragment.newInstance(numMillis)).commit();
+ }
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // in automotive mode, there's no system wide back button, so need to add that
+ if (DeviceUtils.isAuto(this)) {
+ onBackPressed();
+ } else {
+ finish();
+ }
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
}
}
diff --git a/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionsFragment.java
index 010fa7ad..aac7faf6 100644
--- a/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionsFragment.java
+++ b/src/com/android/packageinstaller/permission/ui/auto/AutoAppPermissionsFragment.java
@@ -23,6 +23,7 @@ import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -35,6 +36,7 @@ import androidx.preference.PreferenceScreen;
import com.android.packageinstaller.auto.AutoSettingsFrameFragment;
import com.android.packageinstaller.permission.model.AppPermissionGroup;
import com.android.packageinstaller.permission.model.AppPermissions;
+import com.android.packageinstaller.permission.model.PermissionUsages;
import com.android.packageinstaller.permission.ui.AppPermissionActivity;
import com.android.packageinstaller.permission.utils.Utils;
import com.android.permissioncontroller.R;
@@ -245,6 +247,7 @@ public class AutoAppPermissionsFragment extends AutoSettingsFrameFragment {
preference.setKey(group.getName());
preference.setTitle(group.getFullLabel());
preference.setIcon(Utils.applyTint(context, icon, android.R.attr.colorControlNormal));
+ preference.setSummary(getPreferenceSummary(group));
preference.setOnPreferenceClickListener(pref -> {
Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION);
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, group.getApp().packageName);
@@ -258,6 +261,37 @@ public class AutoAppPermissionsFragment extends AutoSettingsFrameFragment {
return preference;
}
+ private String getPreferenceSummary(AppPermissionGroup group) {
+ String groupSummary = getGroupSummary(group);
+
+ if (Utils.isModernPermissionGroup(group.getName()) && Utils.shouldShowPermissionUsage(
+ group.getName())) {
+ String lastAccessStr = Utils.getAbsoluteLastUsageString(getContext(),
+ PermissionUsages.loadLastGroupUsage(getContext(), group));
+ if (lastAccessStr != null) {
+ if (group.areRuntimePermissionsGranted()) {
+ return getContext().getString(R.string.app_permission_most_recent_summary,
+ lastAccessStr);
+ } else {
+ return getContext().getString(
+ R.string.app_permission_most_recent_denied_summary, lastAccessStr);
+ }
+ } else {
+ if (TextUtils.isEmpty(groupSummary) && Utils.isPermissionsHubEnabled()) {
+ if (group.areRuntimePermissionsGranted()) {
+ return getContext().getString(
+ R.string.app_permission_never_accessed_summary);
+ } else {
+ return getContext().getString(
+ R.string.app_permission_never_accessed_denied_summary);
+ }
+ }
+ }
+ }
+
+ return groupSummary;
+ }
+
private String getGroupSummary(AppPermissionGroup group) {
if (group.hasPermissionWithBackgroundMode() && group.areRuntimePermissionsGranted()) {
AppPermissionGroup backgroundGroup = group.getBackgroundPermissions();
diff --git a/src/com/android/packageinstaller/permission/ui/auto/AutoPermissionAppsFragment.java b/src/com/android/packageinstaller/permission/ui/auto/AutoPermissionAppsFragment.java
index 9d310006..d4242385 100644
--- a/src/com/android/packageinstaller/permission/ui/auto/AutoPermissionAppsFragment.java
+++ b/src/com/android/packageinstaller/permission/ui/auto/AutoPermissionAppsFragment.java
@@ -33,6 +33,7 @@ import com.android.packageinstaller.auto.AutoSettingsFrameFragment;
import com.android.packageinstaller.permission.model.AppPermissionGroup;
import com.android.packageinstaller.permission.model.PermissionApps;
import com.android.packageinstaller.permission.model.PermissionApps.Callback;
+import com.android.packageinstaller.permission.model.PermissionUsages;
import com.android.packageinstaller.permission.ui.handheld.PermissionAppsFragment;
import com.android.packageinstaller.permission.ui.handheld.PermissionControlPreference;
import com.android.packageinstaller.permission.utils.Utils;
@@ -262,6 +263,10 @@ public class AutoPermissionAppsFragment extends AutoSettingsFrameFragment implem
}
if (existingPref != null) {
+ if (existingPref instanceof PermissionControlPreference) {
+ setPreferenceSummary(group, (PermissionControlPreference) existingPref,
+ category != denied, context);
+ }
category.addPreference(existingPref);
continue;
}
@@ -273,6 +278,7 @@ public class AutoPermissionAppsFragment extends AutoSettingsFrameFragment implem
pref.setTitle(Utils.getFullAppLabel(app.getAppInfo(), context));
pref.setEllipsizeEnd();
pref.useSmallerIcon();
+ setPreferenceSummary(group, pref, category != denied, context);
category.addPreference(pref);
}
@@ -301,4 +307,32 @@ public class AutoPermissionAppsFragment extends AutoSettingsFrameFragment implem
setShowSystemAppsToggle();
setLoading(false);
}
+
+ private void setPreferenceSummary(AppPermissionGroup group, PermissionControlPreference pref,
+ boolean allowed, Context context) {
+ if (!Utils.isModernPermissionGroup(group.getName())) {
+ return;
+ }
+ if (!Utils.shouldShowPermissionUsage(group.getName())) {
+ return;
+ }
+ String lastAccessStr = Utils.getAbsoluteLastUsageString(context,
+ PermissionUsages.loadLastGroupUsage(context, group));
+ if (lastAccessStr != null) {
+ if (allowed) {
+ pref.setSummary(context.getString(R.string.app_permission_most_recent_summary,
+ lastAccessStr));
+ } else {
+ pref.setSummary(
+ context.getString(R.string.app_permission_most_recent_denied_summary,
+ lastAccessStr));
+ }
+ } else if (Utils.isPermissionsHubEnabled()) {
+ if (allowed) {
+ pref.setSummary(R.string.app_permission_never_accessed_summary);
+ } else {
+ pref.setSummary(R.string.app_permission_never_accessed_denied_summary);
+ }
+ }
+ }
}
diff --git a/src/com/android/packageinstaller/permission/ui/auto/ReviewOngoingUsageAutoFragment.java b/src/com/android/packageinstaller/permission/ui/auto/ReviewOngoingUsageAutoFragment.java
new file mode 100644
index 00000000..beeed38b
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/auto/ReviewOngoingUsageAutoFragment.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 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.ui.auto;
+
+import android.app.AlertDialog;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.packageinstaller.permission.ui.handheld.ReviewOngoingUsageFragment;
+
+/**
+ * A dialog listing the currently uses of camera, microphone, and location.
+ */
+public class ReviewOngoingUsageAutoFragment extends ReviewOngoingUsageFragment {
+
+ /**
+ * @return A new {@link ReviewOngoingUsageAutoFragment}
+ */
+ public static ReviewOngoingUsageAutoFragment newInstance(long numMillis) {
+ ReviewOngoingUsageAutoFragment fragment = new ReviewOngoingUsageAutoFragment();
+ Bundle arguments = new Bundle();
+ arguments.putLong(Intent.EXTRA_DURATION_MILLIS, numMillis);
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ @Override
+ protected void setNeutralButton(AlertDialog.Builder builder) {
+ // do nothing
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionFragment.java
index 214d7202..196297ca 100644
--- a/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionFragment.java
+++ b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionFragment.java
@@ -23,6 +23,7 @@ import static com.android.packageinstaller.PermissionControllerStatsLog.APP_PERM
import static java.lang.annotation.RetentionPolicy.SOURCE;
+import android.Manifest;
import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
@@ -59,6 +60,7 @@ import androidx.fragment.app.Fragment;
import com.android.packageinstaller.PermissionControllerStatsLog;
import com.android.packageinstaller.permission.model.AppPermissionGroup;
import com.android.packageinstaller.permission.model.Permission;
+import com.android.packageinstaller.permission.model.PermissionUsages;
import com.android.packageinstaller.permission.ui.AppPermissionActivity;
import com.android.packageinstaller.permission.utils.LocationUtils;
import com.android.packageinstaller.permission.utils.PackageRemovalMonitor;
@@ -203,7 +205,19 @@ public class AppPermissionFragment extends SettingsWithLargeHeader {
((TextView) root.requireViewById(R.id.permission_message)).setText(
context.getString(R.string.app_permission_header, mGroup.getFullLabel()));
- root.requireViewById(R.id.usage_summary).setVisibility(View.GONE);
+ if (!Utils.isPermissionsHubEnabled()) {
+ root.requireViewById(R.id.usage_summary).setVisibility(View.GONE);
+ } else if (Utils.isModernPermissionGroup(mGroup.getName())) {
+ if (!Utils.shouldShowPermissionUsage(mGroup.getName())) {
+ ((TextView) root.requireViewById(R.id.usage_summary)).setText(
+ context.getString(R.string.app_permission_footer_not_available));
+ } else {
+ ((TextView) root.requireViewById(R.id.usage_summary)).setText(
+ getUsageSummary(context, appLabel));
+ }
+ } else {
+ root.requireViewById(R.id.usage_summary).setVisibility(View.GONE);
+ }
long sessionId = getArguments().getLong(EXTRA_SESSION_ID);
TextView footer1Link = root.requireViewById(R.id.footer_link_1);
@@ -247,6 +261,93 @@ public class AppPermissionFragment extends SettingsWithLargeHeader {
return root;
}
+ private @NonNull String getUsageSummary(@NonNull Context context, @NonNull String appLabel) {
+ String timeDiffStr = Utils.getRelativeLastUsageString(context,
+ PermissionUsages.loadLastGroupUsage(context, mGroup));
+ int strResId;
+ if (timeDiffStr == null) {
+ switch (mGroup.getName()) {
+ case Manifest.permission_group.ACTIVITY_RECOGNITION:
+ strResId = R.string.app_permission_footer_no_usages_activity_recognition;
+ break;
+ case Manifest.permission_group.CALENDAR:
+ strResId = R.string.app_permission_footer_no_usages_calendar;
+ break;
+ case Manifest.permission_group.CALL_LOG:
+ strResId = R.string.app_permission_footer_no_usages_call_log;
+ break;
+ case Manifest.permission_group.CAMERA:
+ strResId = R.string.app_permission_footer_no_usages_camera;
+ break;
+ case Manifest.permission_group.CONTACTS:
+ strResId = R.string.app_permission_footer_no_usages_contacts;
+ break;
+ case Manifest.permission_group.LOCATION:
+ strResId = R.string.app_permission_footer_no_usages_location;
+ break;
+ case Manifest.permission_group.MICROPHONE:
+ strResId = R.string.app_permission_footer_no_usages_microphone;
+ break;
+ case Manifest.permission_group.PHONE:
+ strResId = R.string.app_permission_footer_no_usages_phone;
+ break;
+ case Manifest.permission_group.SENSORS:
+ strResId = R.string.app_permission_footer_no_usages_sensors;
+ break;
+ case Manifest.permission_group.SMS:
+ strResId = R.string.app_permission_footer_no_usages_sms;
+ break;
+ case Manifest.permission_group.STORAGE:
+ strResId = R.string.app_permission_footer_no_usages_storage;
+ break;
+ default:
+ return context.getString(R.string.app_permission_footer_no_usages_generic,
+ appLabel, mGroup.getLabel().toString().toLowerCase());
+ }
+ return context.getString(strResId, appLabel);
+ } else {
+ switch (mGroup.getName()) {
+ case Manifest.permission_group.ACTIVITY_RECOGNITION:
+ strResId = R.string.app_permission_footer_usage_summary_activity_recognition;
+ break;
+ case Manifest.permission_group.CALENDAR:
+ strResId = R.string.app_permission_footer_usage_summary_calendar;
+ break;
+ case Manifest.permission_group.CALL_LOG:
+ strResId = R.string.app_permission_footer_usage_summary_call_log;
+ break;
+ case Manifest.permission_group.CAMERA:
+ strResId = R.string.app_permission_footer_usage_summary_camera;
+ break;
+ case Manifest.permission_group.CONTACTS:
+ strResId = R.string.app_permission_footer_usage_summary_contacts;
+ break;
+ case Manifest.permission_group.LOCATION:
+ strResId = R.string.app_permission_footer_usage_summary_location;
+ break;
+ case Manifest.permission_group.MICROPHONE:
+ strResId = R.string.app_permission_footer_usage_summary_microphone;
+ break;
+ case Manifest.permission_group.PHONE:
+ strResId = R.string.app_permission_footer_usage_summary_phone;
+ break;
+ case Manifest.permission_group.SENSORS:
+ strResId = R.string.app_permission_footer_usage_summary_sensors;
+ break;
+ case Manifest.permission_group.SMS:
+ strResId = R.string.app_permission_footer_usage_summary_sms;
+ break;
+ case Manifest.permission_group.STORAGE:
+ strResId = R.string.app_permission_footer_usage_summary_storage;
+ break;
+ default:
+ return context.getString(R.string.app_permission_footer_usage_summary_generic,
+ appLabel, mGroup.getLabel().toString().toLowerCase(), timeDiffStr);
+ }
+ return context.getString(strResId, appLabel, timeDiffStr);
+ }
+ }
+
@Override
public void onStart() {
super.onStart();
diff --git a/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java
index 2aa3072a..c8166b0f 100644
--- a/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java
+++ b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java
@@ -51,6 +51,7 @@ import androidx.preference.PreferenceScreen;
import com.android.packageinstaller.PermissionControllerStatsLog;
import com.android.packageinstaller.permission.model.AppPermissionGroup;
import com.android.packageinstaller.permission.model.AppPermissions;
+import com.android.packageinstaller.permission.model.PermissionUsages;
import com.android.packageinstaller.permission.utils.Utils;
import com.android.permissioncontroller.R;
import com.android.settingslib.HelpUtils;
@@ -248,7 +249,34 @@ public final class AppPermissionsFragment extends SettingsWithLargeHeader {
preference.setIcon(Utils.applyTint(context, icon,
android.R.attr.colorControlNormal));
preference.setTitle(group.getFullLabel());
- preference.setGroupSummary(group);
+ if (Utils.isModernPermissionGroup(group.getName()) && Utils.shouldShowPermissionUsage(
+ group.getName())) {
+ String lastAccessStr = Utils.getAbsoluteLastUsageString(context,
+ PermissionUsages.loadLastGroupUsage(context, group));
+ if (lastAccessStr != null) {
+ if (group.areRuntimePermissionsGranted()) {
+ preference.setSummary(
+ context.getString(R.string.app_permission_most_recent_summary,
+ lastAccessStr));
+ } else {
+ preference.setSummary(context.getString(
+ R.string.app_permission_most_recent_denied_summary, lastAccessStr));
+ }
+ } else {
+ preference.setGroupSummary(group);
+ if (preference.getSummary().length() == 0 && Utils.isPermissionsHubEnabled()) {
+ if (group.areRuntimePermissionsGranted()) {
+ preference.setSummary(context.getString(
+ R.string.app_permission_never_accessed_summary));
+ } else {
+ preference.setSummary(context.getString(
+ R.string.app_permission_never_accessed_denied_summary));
+ }
+ }
+ }
+ } else {
+ preference.setGroupSummary(group);
+ }
if (isPlatform) {
PreferenceCategory category =
diff --git a/src/com/android/packageinstaller/permission/ui/handheld/ExpandablePreferenceGroup.java b/src/com/android/packageinstaller/permission/ui/handheld/ExpandablePreferenceGroup.java
new file mode 100644
index 00000000..49a2d5f6
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/handheld/ExpandablePreferenceGroup.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2018 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.ui.handheld;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.permissioncontroller.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A preference group that expands/collapses its children when clicked.
+ */
+public class ExpandablePreferenceGroup extends PreferenceGroup {
+ private @NonNull Context mContext;
+ private @NonNull List<Preference> mPreferences;
+ private @NonNull List<Pair<Integer, CharSequence>> mSummaryIcons;
+ private boolean mExpanded;
+
+ public ExpandablePreferenceGroup(@NonNull Context context) {
+ super(context, null);
+
+ mContext = context;
+ mPreferences = new ArrayList<>();
+ mSummaryIcons = new ArrayList<>();
+ mExpanded = false;
+
+ setLayoutResource(R.layout.preference_usage);
+ setWidgetLayoutResource(R.layout.image_view);
+ setOnPreferenceClickListener(preference -> {
+ if (!mExpanded) {
+ int numPreferences = mPreferences.size();
+ for (int i = 0; i < numPreferences; i++) {
+ super.addPreference(mPreferences.get(i));
+ }
+ } else {
+ removeAll();
+ }
+ mExpanded = !mExpanded;
+ return true;
+ });
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ ImageView icon = (ImageView) holder.findViewById(android.R.id.icon);
+ int rightIconSize = mContext.getResources().getDimensionPixelSize(
+ R.dimen.secondary_app_icon_size);
+ icon.setMaxWidth(rightIconSize);
+ icon.setMaxHeight(rightIconSize);
+
+ super.onBindViewHolder(holder);
+
+ TextView summary = (TextView) holder.findViewById(android.R.id.summary);
+ summary.setMaxLines(1);
+ summary.setEllipsize(TextUtils.TruncateAt.END);
+
+ ImageView rightImageView = holder.findViewById(
+ android.R.id.widget_frame).findViewById(R.id.icon);
+ if (mExpanded) {
+ rightImageView.setImageResource(R.drawable.ic_arrow_up);
+ } else {
+ rightImageView.setImageResource(R.drawable.ic_arrow_down);
+ }
+
+ holder.setDividerAllowedAbove(false);
+ holder.setDividerAllowedBelow(false);
+
+ holder.findViewById(R.id.title_widget_frame).setVisibility(View.GONE);
+
+ ViewGroup summaryFrame = (ViewGroup) holder.findViewById(R.id.summary_widget_frame);
+ if (mSummaryIcons.isEmpty()) {
+ summaryFrame.setVisibility(View.GONE);
+ } else {
+ summaryFrame.removeAllViews();
+ int numIcons = mSummaryIcons.size();
+ for (int i = 0; i < numIcons; i++) {
+ LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class);
+ ViewGroup group = (ViewGroup) inflater.inflate(R.layout.title_summary_image_view,
+ null);
+ ImageView imageView = group.requireViewById(R.id.icon);
+ Pair<Integer, CharSequence> summaryIcons = mSummaryIcons.get(i);
+ imageView.setImageResource(summaryIcons.first);
+ if (summaryIcons.second != null) {
+ imageView.setContentDescription(summaryIcons.second);
+ }
+ summaryFrame.addView(group);
+ }
+ }
+ }
+
+ @Override
+ public boolean addPreference(Preference preference) {
+ mPreferences.add(preference);
+ return true;
+ }
+
+ /**
+ * Show the given icon next to this preference's summary.
+ *
+ * @param resId the resourceId of the drawable to use as the icon.
+ */
+ public void addSummaryIcon(@DrawableRes int resId, @Nullable CharSequence contentDescription) {
+ mSummaryIcons.add(Pair.create(resId, contentDescription));
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/handheld/PermissionAppsFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/PermissionAppsFragment.java
index 304aa38a..a34420d4 100644
--- a/src/com/android/packageinstaller/permission/ui/handheld/PermissionAppsFragment.java
+++ b/src/com/android/packageinstaller/permission/ui/handheld/PermissionAppsFragment.java
@@ -48,6 +48,7 @@ import com.android.packageinstaller.permission.model.AppPermissionGroup;
import com.android.packageinstaller.permission.model.PermissionApps;
import com.android.packageinstaller.permission.model.PermissionApps.Callback;
import com.android.packageinstaller.permission.model.PermissionApps.PermissionApp;
+import com.android.packageinstaller.permission.model.PermissionUsages;
import com.android.packageinstaller.permission.utils.Utils;
import com.android.permissioncontroller.R;
import com.android.settingslib.HelpUtils;
@@ -320,6 +321,10 @@ public final class PermissionAppsFragment extends SettingsWithLargeHeader implem
}
if (existingPref != null) {
+ if (existingPref instanceof PermissionControlPreference) {
+ setPreferenceSummary(group, (PermissionControlPreference) existingPref,
+ category != denied, context);
+ }
category.addPreference(existingPref);
continue;
}
@@ -331,6 +336,7 @@ public final class PermissionAppsFragment extends SettingsWithLargeHeader implem
pref.setTitle(Utils.getFullAppLabel(app.getAppInfo(), context));
pref.setEllipsizeEnd();
pref.useSmallerIcon();
+ setPreferenceSummary(group, pref, category != denied, context);
if (isSystemApp && isTelevision) {
if (mExtraScreen == null) {
@@ -404,6 +410,18 @@ public final class PermissionAppsFragment extends SettingsWithLargeHeader implem
denied.addPreference(empty);
}
+ if (!Utils.shouldShowPermissionUsage(mPermissionApps.getGroupName())
+ && findPreference(KEY_FOOTER) == null) {
+ PreferenceCategory footer = new PreferenceCategory(context);
+ footer.setKey(KEY_FOOTER);
+ getPreferenceScreen().addPreference(footer);
+ Preference footerText = new Preference(context);
+ footerText.setSummary(context.getString(R.string.app_permission_footer_not_available));
+ footerText.setIcon(R.drawable.ic_info_outline);
+ footerText.setSelectable(false);
+ footer.addPreference(footerText);
+ }
+
setLoading(false /* loading */, true /* animate */);
if (mOnPermissionsLoadedListener != null) {
@@ -433,6 +451,35 @@ public final class PermissionAppsFragment extends SettingsWithLargeHeader implem
+ " category=" + category);
};
+ private void setPreferenceSummary(AppPermissionGroup group, PermissionControlPreference pref,
+ boolean allowed, Context context) {
+ if (!Utils.isModernPermissionGroup(group.getName())) {
+ return;
+ }
+ if (!Utils.shouldShowPermissionUsage(group.getName())) {
+ return;
+ }
+ String lastAccessStr = Utils.getAbsoluteLastUsageString(context,
+ PermissionUsages.loadLastGroupUsage(context, group));
+ if (lastAccessStr != null) {
+ if (allowed) {
+ pref.setSummary(context.getString(R.string.app_permission_most_recent_summary,
+ lastAccessStr));
+ } else {
+ pref.setSummary(
+ context.getString(R.string.app_permission_most_recent_denied_summary,
+ lastAccessStr));
+ }
+ } else if (Utils.isPermissionsHubEnabled()) {
+ if (allowed) {
+ pref.setSummary(context.getString(R.string.app_permission_never_accessed_summary));
+ } else {
+ pref.setSummary(
+ context.getString(R.string.app_permission_never_accessed_denied_summary));
+ }
+ }
+ }
+
public static class SystemAppsFragment extends SettingsWithLargeHeader implements Callback {
PermissionAppsFragment mOuterFragment;
diff --git a/src/com/android/packageinstaller/permission/ui/handheld/PermissionControlPreference.java b/src/com/android/packageinstaller/permission/ui/handheld/PermissionControlPreference.java
index 932cf52b..da86a6d5 100644
--- a/src/com/android/packageinstaller/permission/ui/handheld/PermissionControlPreference.java
+++ b/src/com/android/packageinstaller/permission/ui/handheld/PermissionControlPreference.java
@@ -34,6 +34,7 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.packageinstaller.permission.model.AppPermissionGroup;
+import com.android.packageinstaller.permission.model.AppPermissionUsage.GroupUsage;
import com.android.packageinstaller.permission.ui.AppPermissionActivity;
import com.android.permissioncontroller.R;
@@ -123,6 +124,22 @@ public class PermissionControlPreference extends Preference {
}
/**
+ * Sets this preference's summary based on its permission usage.
+ *
+ * @param groupUsage the usage information
+ * @param accessTimeStr the string representing the last access time
+ */
+ public void setUsageSummary(@NonNull GroupUsage groupUsage, @NonNull String accessTimeStr) {
+ if (groupUsage.getLastAccessForegroundTime() >= groupUsage.getLastAccessBackgroundTime()) {
+ setSummary(mContext.getString(R.string.permission_usage_summary_foreground,
+ accessTimeStr));
+ } else {
+ setSummary(mContext.getString(R.string.permission_usage_summary_background,
+ accessTimeStr));
+ }
+ }
+
+ /**
* Sets this preference to show the given icons to the left of its title.
*
* @param titleIcons the icons to show.
diff --git a/src/com/android/packageinstaller/permission/ui/handheld/PermissionUsageFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/PermissionUsageFragment.java
new file mode 100644
index 00000000..3a302ad0
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/handheld/PermissionUsageFragment.java
@@ -0,0 +1,1056 @@
+/*
+ * Copyright (C) 2018 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.ui.handheld;
+
+import static android.Manifest.permission_group.CAMERA;
+import static android.Manifest.permission_group.LOCATION;
+import static android.Manifest.permission_group.MICROPHONE;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+
+import com.android.packageinstaller.permission.model.AppPermissionGroup;
+import com.android.packageinstaller.permission.model.AppPermissionUsage;
+import com.android.packageinstaller.permission.model.AppPermissionUsage.GroupUsage;
+import com.android.packageinstaller.permission.model.PermissionApps;
+import com.android.packageinstaller.permission.model.PermissionApps.PermissionApp;
+import com.android.packageinstaller.permission.model.PermissionUsages;
+import com.android.packageinstaller.permission.ui.AdjustUserSensitiveActivity;
+import com.android.packageinstaller.permission.utils.Utils;
+import com.android.permissioncontroller.R;
+import com.android.settingslib.HelpUtils;
+import com.android.settingslib.widget.ActionBarShadowController;
+import com.android.settingslib.widget.BarChartInfo;
+import com.android.settingslib.widget.BarChartPreference;
+import com.android.settingslib.widget.BarViewInfo;
+
+import java.lang.annotation.Retention;
+import java.text.Collator;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Show the usage of all apps of all permission groups.
+ *
+ * <p>Shows a filterable list of app usage of permission groups, each of which links to
+ * AppPermissionsFragment.
+ */
+public class PermissionUsageFragment extends SettingsWithLargeHeader implements
+ PermissionUsages.PermissionsUsagesChangeCallback {
+ private static final String LOG_TAG = "PermissionUsageFragment";
+
+ @Retention(SOURCE)
+ @IntDef(value = {SORT_RECENT, SORT_RECENT_APPS})
+ @interface SortOption {}
+ static final int SORT_RECENT = 1;
+ static final int SORT_RECENT_APPS = 2;
+
+ private static final int MENU_SORT_BY_APP = MENU_HIDE_SYSTEM + 1;
+ private static final int MENU_SORT_BY_TIME = MENU_HIDE_SYSTEM + 2;
+ private static final int MENU_FILTER_BY_PERMISSIONS = MENU_HIDE_SYSTEM + 3;
+ private static final int MENU_FILTER_BY_TIME = MENU_HIDE_SYSTEM + 4;
+ private static final int MENU_REFRESH = MENU_HIDE_SYSTEM + 5;
+ private static final int MENU_ADJUST_USER_SENSITIVE = MENU_HIDE_SYSTEM + 6;
+
+ private static final String KEY_SHOW_SYSTEM_PREFS = "_show_system";
+ private static final String SHOW_SYSTEM_KEY = PermissionUsageFragment.class.getName()
+ + KEY_SHOW_SYSTEM_PREFS;
+ private static final String KEY_PERM_NAME = "_perm_name";
+ private static final String PERM_NAME_KEY = PermissionUsageFragment.class.getName()
+ + KEY_PERM_NAME;
+ private static final String KEY_TIME_INDEX = "_time_index";
+ private static final String TIME_INDEX_KEY = PermissionUsageFragment.class.getName()
+ + KEY_TIME_INDEX;
+ private static final String KEY_SORT = "_sort";
+ private static final String SORT_KEY = PermissionUsageFragment.class.getName()
+ + KEY_SORT;
+
+ /**
+ * The maximum number of columns shown in the bar chart.
+ */
+ private static final int MAXIMUM_NUM_BARS = 4;
+
+ private @NonNull PermissionUsages mPermissionUsages;
+ private @Nullable List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>();
+
+ private Collator mCollator;
+
+ private @NonNull List<TimeFilterItem> mFilterTimes;
+ private int mFilterTimeIndex;
+ private String mFilterGroup;
+ private @SortOption int mSort;
+
+ private boolean mShowSystem;
+ private boolean mHasSystemApps;
+ private MenuItem mShowSystemMenu;
+ private MenuItem mHideSystemMenu;
+ private MenuItem mSortByApp;
+ private MenuItem mSortByTime;
+
+ private ArrayMap<String, Integer> mGroupAppCounts = new ArrayMap<>();
+
+ private boolean mFinishedInitialLoad;
+
+ /**
+ * @return A new fragment
+ */
+ public static @NonNull PermissionUsageFragment newInstance(@Nullable String groupName,
+ long numMillis) {
+ PermissionUsageFragment fragment = new PermissionUsageFragment();
+ Bundle arguments = new Bundle();
+ if (groupName != null) {
+ arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName);
+ }
+ arguments.putLong(Intent.EXTRA_DURATION_MILLIS, numMillis);
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mFinishedInitialLoad = false;
+ mSort = SORT_RECENT_APPS;
+ mFilterGroup = null;
+ initializeTimeFilter();
+ if (savedInstanceState != null) {
+ mShowSystem = savedInstanceState.getBoolean(SHOW_SYSTEM_KEY);
+ mFilterGroup = savedInstanceState.getString(PERM_NAME_KEY);
+ mFilterTimeIndex = savedInstanceState.getInt(TIME_INDEX_KEY);
+ mSort = savedInstanceState.getInt(SORT_KEY);
+ }
+
+ setLoading(true, false);
+ setHasOptionsMenu(true);
+ ActionBar ab = getActivity().getActionBar();
+ if (ab != null) {
+ ab.setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (mFilterGroup == null) {
+ mFilterGroup = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
+ }
+
+ Context context = getPreferenceManager().getContext();
+ mCollator = Collator.getInstance(
+ context.getResources().getConfiguration().getLocales().get(0));
+ mPermissionUsages = new PermissionUsages(context);
+
+ reloadData();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ getActivity().setTitle(R.string.permission_usage_title);
+ }
+
+ /**
+ * Initialize the time filter to show the smallest entry greater than the time passed in as an
+ * argument. If nothing is passed, this simply initializes the possible values.
+ */
+ private void initializeTimeFilter() {
+ Context context = getPreferenceManager().getContext();
+ mFilterTimes = new ArrayList<>();
+ mFilterTimes.add(new TimeFilterItem(Long.MAX_VALUE,
+ context.getString(R.string.permission_usage_any_time),
+ R.string.permission_usage_list_title_any_time,
+ R.string.permission_usage_bar_chart_title_any_time));
+ mFilterTimes.add(new TimeFilterItem(DAYS.toMillis(7),
+ context.getString(R.string.permission_usage_last_7_days),
+ R.string.permission_usage_list_title_last_7_days,
+ R.string.permission_usage_bar_chart_title_last_7_days));
+ mFilterTimes.add(new TimeFilterItem(DAYS.toMillis(1),
+ context.getString(R.string.permission_usage_last_day),
+ R.string.permission_usage_list_title_last_day,
+ R.string.permission_usage_bar_chart_title_last_day));
+ mFilterTimes.add(new TimeFilterItem(HOURS.toMillis(1),
+ context.getString(R.string.permission_usage_last_hour),
+ R.string.permission_usage_list_title_last_hour,
+ R.string.permission_usage_bar_chart_title_last_hour));
+ mFilterTimes.add(new TimeFilterItem(MINUTES.toMillis(15),
+ context.getString(R.string.permission_usage_last_15_minutes),
+ R.string.permission_usage_list_title_last_15_minutes,
+ R.string.permission_usage_bar_chart_title_last_15_minutes));
+ mFilterTimes.add(new TimeFilterItem(MINUTES.toMillis(1),
+ context.getString(R.string.permission_usage_last_minute),
+ R.string.permission_usage_list_title_last_minute,
+ R.string.permission_usage_bar_chart_title_last_minute));
+
+ long numMillis = getArguments().getLong(Intent.EXTRA_DURATION_MILLIS);
+ long supremum = Long.MAX_VALUE;
+ int supremumIndex = -1;
+ int numTimes = mFilterTimes.size();
+ for (int i = 0; i < numTimes; i++) {
+ long curTime = mFilterTimes.get(i).getTime();
+ if (curTime >= numMillis && curTime <= supremum) {
+ supremum = curTime;
+ supremumIndex = i;
+ }
+ }
+ if (supremumIndex != -1) {
+ mFilterTimeIndex = supremumIndex;
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(SHOW_SYSTEM_KEY, mShowSystem);
+ outState.putString(PERM_NAME_KEY, mFilterGroup);
+ outState.putInt(TIME_INDEX_KEY, mFilterTimeIndex);
+ outState.putInt(SORT_KEY, mSort);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ mSortByApp = menu.add(Menu.NONE, MENU_SORT_BY_APP, Menu.NONE, R.string.sort_by_app);
+ mSortByTime = menu.add(Menu.NONE, MENU_SORT_BY_TIME, Menu.NONE, R.string.sort_by_time);
+ menu.add(Menu.NONE, MENU_FILTER_BY_PERMISSIONS, Menu.NONE, R.string.filter_by_permissions);
+ menu.add(Menu.NONE, MENU_FILTER_BY_TIME, Menu.NONE, R.string.filter_by_time);
+ if (mHasSystemApps) {
+ mShowSystemMenu = menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE,
+ R.string.menu_show_system);
+ mHideSystemMenu = menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE,
+ R.string.menu_hide_system);
+ }
+
+ menu.add(Menu.NONE, MENU_ADJUST_USER_SENSITIVE, Menu.NONE,
+ R.string.menu_adjust_user_sensitive);
+
+ HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_permission_usage,
+ getClass().getName());
+ MenuItem refresh = menu.add(Menu.NONE, MENU_REFRESH, Menu.NONE,
+ R.string.permission_usage_refresh);
+ refresh.setIcon(R.drawable.ic_refresh);
+ refresh.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ updateMenu();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ getActivity().finish();
+ return true;
+ case MENU_SORT_BY_APP:
+ mSort = SORT_RECENT_APPS;
+ updateUI();
+ updateMenu();
+ break;
+ case MENU_SORT_BY_TIME:
+ mSort = SORT_RECENT;
+ updateUI();
+ updateMenu();
+ break;
+ case MENU_FILTER_BY_PERMISSIONS:
+ showPermissionFilterDialog();
+ break;
+ case MENU_FILTER_BY_TIME:
+ showTimeFilterDialog();
+ break;
+ case MENU_SHOW_SYSTEM:
+ case MENU_HIDE_SYSTEM:
+ mShowSystem = item.getItemId() == MENU_SHOW_SYSTEM;
+ // We already loaded all data, so don't reload
+ updateUI();
+ updateMenu();
+ break;
+ case MENU_REFRESH:
+ reloadData();
+ break;
+ case MENU_ADJUST_USER_SENSITIVE:
+ getActivity().startActivity(
+ new Intent(getContext(), AdjustUserSensitiveActivity.class));
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void updateMenu() {
+ if (mHasSystemApps) {
+ mShowSystemMenu.setVisible(!mShowSystem);
+ mHideSystemMenu.setVisible(mShowSystem);
+ }
+ mSortByApp.setVisible(mSort != SORT_RECENT_APPS);
+ mSortByTime.setVisible(mSort != SORT_RECENT);
+ }
+
+ @Override
+ public void onPermissionUsagesChanged() {
+ if (!Utils.isPermissionsHubEnabled()) {
+ setLoading(false, true);
+ return;
+ }
+ if (mPermissionUsages.getUsages().isEmpty()) {
+ return;
+ }
+ mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages());
+
+ // Ensure the group name is valid.
+ if (getGroup(mFilterGroup) == null) {
+ mFilterGroup = null;
+ }
+
+ updateUI();
+ }
+
+ @Override
+ public int getEmptyViewString() {
+ return R.string.no_permission_usages;
+ }
+
+ private void updateUI() {
+ if (mAppPermissionUsages.isEmpty() || getActivity() == null) {
+ return;
+ }
+ Context context = getActivity();
+
+ PreferenceScreen screen = getPreferenceScreen();
+ if (screen == null) {
+ screen = getPreferenceManager().createPreferenceScreen(context);
+ setPreferenceScreen(screen);
+ }
+ screen.removeAll();
+
+ boolean seenSystemApp = false;
+
+ final TimeFilterItem timeFilterItem = mFilterTimes.get(mFilterTimeIndex);
+ long curTime = System.currentTimeMillis();
+ long startTime = Math.max(timeFilterItem == null ? 0 : (curTime - timeFilterItem.getTime()),
+ Instant.EPOCH.toEpochMilli());
+
+ List<Pair<AppPermissionUsage, GroupUsage>> usages = new ArrayList<>();
+ mGroupAppCounts.clear();
+ ArrayList<PermissionApp> permApps = new ArrayList<>();
+ int numApps = mAppPermissionUsages.size();
+ for (int appNum = 0; appNum < numApps; appNum++) {
+ AppPermissionUsage appUsage = mAppPermissionUsages.get(appNum);
+ boolean used = false;
+ List<GroupUsage> appGroups = appUsage.getGroupUsages();
+ int numGroups = appGroups.size();
+ for (int groupNum = 0; groupNum < numGroups; groupNum++) {
+ GroupUsage groupUsage = appGroups.get(groupNum);
+ long lastAccessTime = groupUsage.getLastAccessTime();
+
+ if (lastAccessTime == 0) {
+ Log.w(LOG_TAG,
+ "Unexpected access time of 0 for " + appUsage.getApp().getKey() + " "
+ + groupUsage.getGroup().getName());
+ continue;
+ }
+ if (lastAccessTime < startTime) {
+ continue;
+ }
+ final boolean isSystemApp = !Utils.isGroupOrBgGroupUserSensitive(
+ groupUsage.getGroup());
+ seenSystemApp = seenSystemApp || isSystemApp;
+ if (isSystemApp && !mShowSystem) {
+ continue;
+ }
+
+ used = true;
+ addGroupUser(groupUsage.getGroup().getName());
+
+ // Filter out usages that aren't of the filtered permission group.
+ // We do this after we call addGroupUser so we compute the correct usage counts
+ // for the permission filter dialog but before we add the usage to our list.
+ if (mFilterGroup != null && !mFilterGroup.equals(groupUsage.getGroup().getName())) {
+ continue;
+ }
+
+ usages.add(Pair.create(appUsage, appGroups.get(groupNum)));
+ }
+ if (used) {
+ permApps.add(appUsage.getApp());
+ addGroupUser(null);
+ }
+ }
+
+ if (mHasSystemApps != seenSystemApp) {
+ mHasSystemApps = seenSystemApp;
+ getActivity().invalidateOptionsMenu();
+ }
+
+ // Update header.
+ if (mFilterGroup == null) {
+ screen.addPreference(createBarChart(usages, timeFilterItem, context));
+ hideHeader();
+ } else {
+ AppPermissionGroup group = getGroup(mFilterGroup);
+ if (group != null) {
+ setHeader(Utils.applyTint(context, context.getDrawable(group.getIconResId()),
+ android.R.attr.colorControlNormal),
+ context.getString(R.string.app_permission_usage_filter_label,
+ group.getLabel()), null, null, true);
+ setSummary(context.getString(R.string.app_permission_usage_remove_filter), v -> {
+ onPermissionGroupSelected(null);
+ });
+ }
+ }
+
+ // Add the preference header.
+ PreferenceCategory category = new PreferenceCategory(context);
+ screen.addPreference(category);
+ if (timeFilterItem != null) {
+ category.setTitle(timeFilterItem.getListTitleRes());
+ }
+
+ // Sort the apps.
+ if (mSort == SORT_RECENT) {
+ usages.sort(PermissionUsageFragment::compareAccessRecency);
+ } else if (mSort == SORT_RECENT_APPS) {
+ if (mFilterGroup == null) {
+ usages.sort(PermissionUsageFragment::compareAccessAppRecency);
+ } else {
+ usages.sort(PermissionUsageFragment::compareAccessTime);
+ }
+ } else {
+ Log.w(LOG_TAG, "Unexpected sort option: " + mSort);
+ }
+
+ // If there are no entries, don't show anything.
+ if (usages.isEmpty()) {
+ screen.removeAll();
+ }
+
+ new PermissionApps.AppDataLoader(context, () -> {
+ ExpandablePreferenceGroup parent = null;
+ AppPermissionUsage lastAppPermissionUsage = null;
+ String lastAccessTimeString = null;
+ List<CharSequence> groups = new ArrayList<>();
+
+ final int numUsages = usages.size();
+ for (int usageNum = 0; usageNum < numUsages; usageNum++) {
+ final Pair<AppPermissionUsage, GroupUsage> usage = usages.get(usageNum);
+ AppPermissionUsage appPermissionUsage = usage.first;
+ GroupUsage groupUsage = usage.second;
+
+ String accessTimeString = Utils.getAbsoluteLastUsageString(context, groupUsage);
+
+ if (lastAppPermissionUsage != appPermissionUsage || (mSort == SORT_RECENT
+ && !accessTimeString.equals(lastAccessTimeString))) {
+ setPermissionSummary(parent, groups);
+ // Add a "parent" entry for the app that will expand to the individual entries.
+ parent = createExpandablePreferenceGroup(context, appPermissionUsage,
+ mSort == SORT_RECENT ? accessTimeString : null);
+ category.addPreference(parent);
+ lastAppPermissionUsage = appPermissionUsage;
+ groups = new ArrayList<>();
+ }
+
+ parent.addPreference(createPermissionUsagePreference(context, appPermissionUsage,
+ groupUsage, accessTimeString));
+ groups.add(groupUsage.getGroup().getLabel());
+ lastAccessTimeString = accessTimeString;
+ }
+
+ setPermissionSummary(parent, groups);
+
+ setLoading(false, true);
+ mFinishedInitialLoad = true;
+ setProgressBarVisible(false);
+ mPermissionUsages.stopLoader(getActivity().getLoaderManager());
+ }).execute(permApps.toArray(new PermissionApps.PermissionApp[permApps.size()]));
+ }
+
+ private void addGroupUser(String app) {
+ Integer count = mGroupAppCounts.get(app);
+ if (count == null) {
+ mGroupAppCounts.put(app, 1);
+ } else {
+ mGroupAppCounts.put(app, count + 1);
+ }
+ }
+
+ private void setPermissionSummary(@NonNull ExpandablePreferenceGroup pref,
+ @NonNull List<CharSequence> groups) {
+ if (pref == null) {
+ return;
+ }
+ StringBuilder sb = new StringBuilder();
+ int numGroups = groups.size();
+ for (int i = 0; i < numGroups; i++) {
+ sb.append(groups.get(i));
+ if (i < numGroups - 1) {
+ sb.append(getString(R.string.item_separator));
+ }
+ }
+ pref.setSummary(sb.toString());
+ }
+
+ /**
+ * Reloads the data to show.
+ */
+ private void reloadData() {
+ final TimeFilterItem timeFilterItem = mFilterTimes.get(mFilterTimeIndex);
+ final long filterTimeBeginMillis = Math.max(System.currentTimeMillis()
+ - timeFilterItem.getTime(), Instant.EPOCH.toEpochMilli());
+ mPermissionUsages.load(null /*filterPackageName*/, null /*filterPermissionGroups*/,
+ filterTimeBeginMillis, Long.MAX_VALUE, PermissionUsages.USAGE_FLAG_LAST
+ | PermissionUsages.USAGE_FLAG_HISTORICAL, getActivity().getLoaderManager(),
+ false /*getUiInfo*/, false /*getNonPlatformPermissions*/, this /*callback*/,
+ false /*sync*/);
+ if (mFinishedInitialLoad) {
+ setProgressBarVisible(true);
+ }
+ }
+ /**
+ * Create a bar chart showing the permissions that are used by the most apps.
+ *
+ * @param usages the usages
+ * @param timeFilterItem the time filter, or null if no filter is set
+ * @param context the context
+ *
+ * @return the Preference representing the bar chart
+ */
+ private BarChartPreference createBarChart(
+ @NonNull List<Pair<AppPermissionUsage, GroupUsage>> usages,
+ @Nullable TimeFilterItem timeFilterItem, @NonNull Context context) {
+ ArrayList<AppPermissionGroup> groups = new ArrayList<>();
+ ArrayMap<String, Integer> groupToAppCount = new ArrayMap<>();
+ int usageCount = usages.size();
+ for (int i = 0; i < usageCount; i++) {
+ Pair<AppPermissionUsage, GroupUsage> usage = usages.get(i);
+ GroupUsage groupUsage = usage.second;
+ Integer count = groupToAppCount.get(groupUsage.getGroup().getName());
+ if (count == null) {
+ groups.add(groupUsage.getGroup());
+ groupToAppCount.put(groupUsage.getGroup().getName(), 1);
+ } else {
+ groupToAppCount.put(groupUsage.getGroup().getName(), count + 1);
+ }
+ }
+
+ groups.sort((x, y) -> {
+ String xName = x.getName();
+ String yName = y.getName();
+ int usageDiff = compareLong(groupToAppCount.get(xName), groupToAppCount.get(yName));
+ if (usageDiff != 0) {
+ return usageDiff;
+ }
+ if (xName.equals(LOCATION)) {
+ return -1;
+ } else if (yName.equals(LOCATION)) {
+ return 1;
+ } else if (xName.equals(MICROPHONE)) {
+ return -1;
+ } else if (yName.equals(MICROPHONE)) {
+ return 1;
+ } else if (xName.equals(CAMERA)) {
+ return -1;
+ } else if (yName.equals(CAMERA)) {
+ return 1;
+ }
+ return x.getName().compareTo(y.getName());
+ });
+
+ BarChartInfo.Builder builder = new BarChartInfo.Builder();
+ if (timeFilterItem != null) {
+ builder.setTitle(timeFilterItem.getGraphTitleRes());
+ }
+
+ int numBarsToShow = Math.min(groups.size(), MAXIMUM_NUM_BARS);
+ for (int i = 0; i < numBarsToShow; i++) {
+ AppPermissionGroup group = groups.get(i);
+ int count = groupToAppCount.get(group.getName());
+ Drawable icon = Utils.applyTint(context,
+ Utils.loadDrawable(context.getPackageManager(), group.getIconPkg(),
+ group.getIconResId()), android.R.attr.colorControlNormal);
+ BarViewInfo barViewInfo = new BarViewInfo(icon, count, group.getLabel(),
+ context.getResources().getQuantityString(R.plurals.permission_usage_bar_label,
+ count, count), group.getLabel());
+ barViewInfo.setClickListener(v -> onPermissionGroupSelected(group.getName()));
+ builder.addBarViewInfo(barViewInfo);
+ }
+
+ BarChartPreference barChart = new BarChartPreference(context, null);
+ barChart.initializeBarChart(builder.build());
+ return barChart;
+ }
+
+ /**
+ * Create an expandable preference group that can hold children.
+ *
+ * @param context the context
+ * @param appPermissionUsage the permission usage for an app
+ *
+ * @return the expandable preference group.
+ */
+ private ExpandablePreferenceGroup createExpandablePreferenceGroup(@NonNull Context context,
+ @NonNull AppPermissionUsage appPermissionUsage, @Nullable String summaryString) {
+ ExpandablePreferenceGroup preference = new ExpandablePreferenceGroup(context);
+ preference.setTitle(appPermissionUsage.getApp().getLabel());
+ preference.setIcon(appPermissionUsage.getApp().getIcon());
+ if (summaryString != null) {
+ preference.setSummary(summaryString);
+ }
+ return preference;
+ }
+
+ /**
+ * Create a preference representing an app's use of a permission
+ *
+ * @param context the context
+ * @param appPermissionUsage the permission usage for the app
+ * @param groupUsage the permission item to add
+ * @param accessTimeStr the string representing the access time
+ *
+ * @return the Preference
+ */
+ private PermissionControlPreference createPermissionUsagePreference(@NonNull Context context,
+ @NonNull AppPermissionUsage appPermissionUsage,
+ @NonNull GroupUsage groupUsage, @NonNull String accessTimeStr) {
+ final PermissionControlPreference pref = new PermissionControlPreference(context,
+ groupUsage.getGroup(), PermissionUsageFragment.class.getName());
+
+ final AppPermissionGroup group = groupUsage.getGroup();
+ pref.setTitle(group.getLabel());
+ pref.setUsageSummary(groupUsage, accessTimeStr);
+ pref.setTitleIcons(Collections.singletonList(group.getIconResId()));
+ pref.setKey(group.getApp().packageName + "," + group.getName());
+ pref.useSmallerIcon();
+ pref.setRightIcon(context.getDrawable(R.drawable.ic_settings_outline));
+ return pref;
+ }
+
+ /**
+ * Compare two usages by whichever app was used most recently. If the two represent the same
+ * app, sort by which group was used most recently.
+ *
+ * Can be used as a {@link java.util.Comparator}.
+ *
+ * @param x a usage.
+ * @param y a usage.
+ *
+ * @return see {@link java.util.Comparator#compare(Object, Object)}.
+ */
+ private static int compareAccessAppRecency(@NonNull Pair<AppPermissionUsage, GroupUsage> x,
+ @NonNull Pair<AppPermissionUsage, GroupUsage> y) {
+ if (x.first.getApp().getKey().equals(y.first.getApp().getKey())) {
+ return compareAccessTime(x.second, y.second);
+ }
+ return compareAccessTime(x.first, y.first);
+ }
+
+ /**
+ * Compare two usages by their access time.
+ *
+ * Can be used as a {@link java.util.Comparator}.
+ *
+ * @param x a usage.
+ * @param y a usage.
+ *
+ * @return see {@link java.util.Comparator#compare(Object, Object)}.
+ */
+ private static int compareAccessTime(@NonNull Pair<AppPermissionUsage, GroupUsage> x,
+ @NonNull Pair<AppPermissionUsage, GroupUsage> y) {
+ return compareAccessTime(x.second, y.second);
+ }
+
+ /**
+ * Compare two usages by their access time.
+ *
+ * Can be used as a {@link java.util.Comparator}.
+ *
+ * @param x a usage.
+ * @param y a usage.
+ *
+ * @return see {@link java.util.Comparator#compare(Object, Object)}.
+ */
+ private static int compareAccessTime(@NonNull GroupUsage x, @NonNull GroupUsage y) {
+ final int timeDiff = compareLong(x.getLastAccessTime(), y.getLastAccessTime());
+ if (timeDiff != 0) {
+ return timeDiff;
+ }
+ // Make sure we lose no data if same
+ return x.hashCode() - y.hashCode();
+ }
+
+ /**
+ * Compare two AppPermissionUsage by their access time.
+ *
+ * Can be used as a {@link java.util.Comparator}.
+ *
+ * @param x an AppPermissionUsage.
+ * @param y an AppPermissionUsage.
+ *
+ * @return see {@link java.util.Comparator#compare(Object, Object)}.
+ */
+ private static int compareAccessTime(@NonNull AppPermissionUsage x,
+ @NonNull AppPermissionUsage y) {
+ final int timeDiff = compareLong(x.getLastAccessTime(), y.getLastAccessTime());
+ if (timeDiff != 0) {
+ return timeDiff;
+ }
+ // Make sure we lose no data if same
+ return x.hashCode() - y.hashCode();
+ }
+
+ /**
+ * Compare two longs.
+ *
+ * Can be used as a {@link java.util.Comparator}.
+ *
+ * @param x the first long.
+ * @param y the second long.
+ *
+ * @return see {@link java.util.Comparator#compare(Object, Object)}.
+ */
+ private static int compareLong(long x, long y) {
+ if (x > y) {
+ return -1;
+ } else if (x < y) {
+ return 1;
+ }
+ return 0;
+ }
+
+ /**
+ * Compare two usages by recency of access.
+ *
+ * Can be used as a {@link java.util.Comparator}.
+ *
+ * @param x a usage.
+ * @param y a usage.
+ *
+ * @return see {@link java.util.Comparator#compare(Object, Object)}.
+ */
+ private static int compareAccessRecency(@NonNull Pair<AppPermissionUsage, GroupUsage> x,
+ @NonNull Pair<AppPermissionUsage, GroupUsage> y) {
+ final int timeDiff = compareAccessTime(x, y);
+ if (timeDiff != 0) {
+ return timeDiff;
+ }
+ // Make sure we lose no data if same
+ return x.hashCode() - y.hashCode();
+ }
+
+ /**
+ * Get the permission groups declared by the OS.
+ *
+ * @return a list of the permission groups declared by the OS.
+ */
+ private @NonNull List<AppPermissionGroup> getOSPermissionGroups() {
+ final List<AppPermissionGroup> groups = new ArrayList<>();
+ final Set<String> seenGroups = new ArraySet<>();
+ final int numGroups = mAppPermissionUsages.size();
+ for (int i = 0; i < numGroups; i++) {
+ final AppPermissionUsage appUsage = mAppPermissionUsages.get(i);
+ final List<GroupUsage> groupUsages = appUsage.getGroupUsages();
+ final int groupUsageCount = groupUsages.size();
+ for (int j = 0; j < groupUsageCount; j++) {
+ final GroupUsage groupUsage = groupUsages.get(j);
+ if (Utils.isModernPermissionGroup(groupUsage.getGroup().getName())) {
+ if (seenGroups.add(groupUsage.getGroup().getName())) {
+ groups.add(groupUsage.getGroup());
+ }
+ }
+ }
+ }
+ return groups;
+ }
+
+ /**
+ * Get an AppPermissionGroup that represents the given permission group (and an arbitrary app).
+ *
+ * @param groupName The name of the permission group.
+ *
+ * @return an AppPermissionGroup rerepsenting the given permission group or null if no such
+ * AppPermissionGroup is found.
+ */
+ private @Nullable AppPermissionGroup getGroup(@NonNull String groupName) {
+ List<AppPermissionGroup> groups = getOSPermissionGroups();
+ int numGroups = groups.size();
+ for (int i = 0; i < numGroups; i++) {
+ if (groups.get(i).getName().equals(groupName)) {
+ return groups.get(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Show a dialog that allows selecting a permission group by which to filter the entries.
+ */
+ private void showPermissionFilterDialog() {
+ Context context = getPreferenceManager().getContext();
+
+ // Get the permission labels.
+ List<AppPermissionGroup> groups = getOSPermissionGroups();
+ groups.sort(
+ (x, y) -> mCollator.compare(x.getLabel().toString(), y.getLabel().toString()));
+
+ // Create the dialog entries.
+ String[] groupNames = new String[groups.size() + 1];
+ CharSequence[] groupLabels = new CharSequence[groupNames.length];
+ int[] groupAccessCounts = new int[groupNames.length];
+ groupNames[0] = null;
+ groupLabels[0] = context.getString(R.string.permission_usage_any_permission);
+ Integer allAccesses = mGroupAppCounts.get(null);
+ if (allAccesses == null) {
+ allAccesses = 0;
+ }
+ groupAccessCounts[0] = allAccesses;
+ int selection = 0;
+ int numGroups = groups.size();
+ for (int i = 0; i < numGroups; i++) {
+ AppPermissionGroup group = groups.get(i);
+ groupNames[i + 1] = group.getName();
+ groupLabels[i + 1] = group.getLabel();
+ Integer appCount = mGroupAppCounts.get(group.getName());
+ if (appCount == null) {
+ appCount = 0;
+ }
+ groupAccessCounts[i + 1] = appCount;
+ if (group.getName().equals(mFilterGroup)) {
+ selection = i + 1;
+ }
+ }
+
+ // Create the dialog
+ Bundle args = new Bundle();
+ args.putCharSequence(PermissionsFilterDialog.TITLE,
+ context.getString(R.string.filter_by_title));
+ args.putCharSequenceArray(PermissionsFilterDialog.ELEMS, groupLabels);
+ args.putInt(PermissionsFilterDialog.SELECTION, selection);
+ args.putStringArray(PermissionsFilterDialog.GROUPS, groupNames);
+ args.putIntArray(PermissionsFilterDialog.ACCESS_COUNTS, groupAccessCounts);
+ PermissionsFilterDialog chooserDialog = new PermissionsFilterDialog();
+ chooserDialog.setArguments(args);
+ chooserDialog.setTargetFragment(this, 0);
+ chooserDialog.show(getFragmentManager().beginTransaction(),
+ PermissionsFilterDialog.class.getName());
+ }
+
+ /**
+ * Callback when the user selects a permission group by which to filter.
+ *
+ * @param selectedGroup The PermissionGroup to use to filter entries, or null if we should show
+ * all entries.
+ */
+ private void onPermissionGroupSelected(@Nullable String selectedGroup) {
+ Fragment frag = newInstance(selectedGroup, mFilterTimes.get(mFilterTimeIndex).getTime());
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, frag)
+ .addToBackStack("PermissionUsage")
+ .commit();
+ }
+
+ /**
+ * A dialog that allows the user to select a permission group by which to filter entries.
+ *
+ * @see #showPermissionFilterDialog()
+ */
+ public static class PermissionsFilterDialog extends DialogFragment {
+ private static final String TITLE = PermissionsFilterDialog.class.getName() + ".arg.title";
+ private static final String ELEMS = PermissionsFilterDialog.class.getName() + ".arg.elems";
+ private static final String SELECTION = PermissionsFilterDialog.class.getName()
+ + ".arg.selection";
+ private static final String GROUPS = PermissionsFilterDialog.class.getName()
+ + ".arg.groups";
+ private static final String ACCESS_COUNTS = PermissionsFilterDialog.class.getName()
+ + ".arg.access_counts";
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder b = new AlertDialog.Builder(getActivity())
+ .setView(createDialogView());
+
+ return b.create();
+ }
+
+ private @NonNull View createDialogView() {
+ PermissionUsageFragment fragment = (PermissionUsageFragment) getTargetFragment();
+ CharSequence[] elems = getArguments().getCharSequenceArray(ELEMS);
+ String[] groups = getArguments().getStringArray(GROUPS);
+ int[] accessCounts = getArguments().getIntArray(ACCESS_COUNTS);
+ int selectedIndex = getArguments().getInt(SELECTION);
+
+ LayoutInflater layoutInflater = LayoutInflater.from(fragment.getActivity());
+ View view = layoutInflater.inflate(R.layout.permission_filter_dialog, null);
+ ViewGroup itemsListView = view.requireViewById(R.id.items_container);
+
+ ((TextView) view.requireViewById(R.id.title)).setText(
+ getArguments().getCharSequence(TITLE));
+
+ ActionBarShadowController.attachToView(view.requireViewById(R.id.title_container),
+ getLifecycle(), view.requireViewById(R.id.scroll_view));
+
+ for (int i = 0; i < elems.length; i++) {
+ String groupName = groups[i];
+ View itemView = layoutInflater.inflate(R.layout.permission_filter_dialog_item,
+ itemsListView, false);
+
+ ((TextView) itemView.requireViewById(R.id.title)).setText(elems[i]);
+ ((TextView) itemView.requireViewById(R.id.summary)).setText(
+ getActivity().getResources().getQuantityString(
+ R.plurals.permission_usage_permission_filter_subtitle,
+ accessCounts[i], accessCounts[i]));
+
+ itemView.setOnClickListener((v) -> {
+ dismissAllowingStateLoss();
+ fragment.onPermissionGroupSelected(groupName);
+ });
+
+ RadioButton radioButton = itemView.requireViewById(R.id.radio_button);
+ radioButton.setChecked(i == selectedIndex);
+ radioButton.setOnClickListener((v) -> {
+ dismissAllowingStateLoss();
+ fragment.onPermissionGroupSelected(groupName);
+ });
+
+ itemsListView.addView(itemView);
+ }
+
+ return view;
+ }
+ }
+
+ private void showTimeFilterDialog() {
+ Context context = getPreferenceManager().getContext();
+
+ CharSequence[] labels = new CharSequence[mFilterTimes.size()];
+ for (int i = 0; i < labels.length; i++) {
+ labels[i] = mFilterTimes.get(i).getLabel();
+ }
+
+ // Create the dialog
+ Bundle args = new Bundle();
+ args.putCharSequence(TimeFilterDialog.TITLE,
+ context.getString(R.string.filter_by_title));
+ args.putCharSequenceArray(TimeFilterDialog.ELEMS, labels);
+ args.putInt(TimeFilterDialog.SELECTION, mFilterTimeIndex);
+ TimeFilterDialog chooserDialog = new TimeFilterDialog();
+ chooserDialog.setArguments(args);
+ chooserDialog.setTargetFragment(this, 0);
+ chooserDialog.show(getFragmentManager().beginTransaction(),
+ TimeFilterDialog.class.getName());
+ }
+
+ /**
+ * Callback when the user selects a time by which to filter.
+ *
+ * @param selectedIndex The index of the dialog option selected by the user.
+ */
+ private void onTimeSelected(int selectedIndex) {
+ mFilterTimeIndex = selectedIndex;
+ reloadData();
+ }
+
+ /**
+ * A dialog that allows the user to select a time by which to filter entries.
+ *
+ * @see #showTimeFilterDialog()
+ */
+ public static class TimeFilterDialog extends DialogFragment {
+ private static final String TITLE = TimeFilterDialog.class.getName() + ".arg.title";
+ private static final String ELEMS = TimeFilterDialog.class.getName() + ".arg.elems";
+ private static final String SELECTION = TimeFilterDialog.class.getName() + ".arg.selection";
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ PermissionUsageFragment fragment = (PermissionUsageFragment) getTargetFragment();
+ CharSequence[] elems = getArguments().getCharSequenceArray(ELEMS);
+ AlertDialog.Builder b = new AlertDialog.Builder(getActivity())
+ .setTitle(getArguments().getCharSequence(TITLE))
+ .setSingleChoiceItems(elems, getArguments().getInt(SELECTION),
+ (dialog, which) -> {
+ dismissAllowingStateLoss();
+ fragment.onTimeSelected(which);
+ }
+ );
+
+ return b.create();
+ }
+ }
+
+ /**
+ * A class representing a given time, e.g., "in the last hour".
+ */
+ private static class TimeFilterItem {
+ private final long mTime;
+ private final @NonNull String mLabel;
+ private final @StringRes int mListTitleRes;
+ private final @StringRes int mGraphTitleRes;
+
+ TimeFilterItem(long time, @NonNull String label, @StringRes int listTitleRes,
+ @StringRes int graphTitleRes) {
+ mTime = time;
+ mLabel = label;
+ mListTitleRes = listTitleRes;
+ mGraphTitleRes = graphTitleRes;
+ }
+
+ /**
+ * Get the time represented by this object in milliseconds.
+ *
+ * @return the time represented by this object.
+ */
+ public long getTime() {
+ return mTime;
+ }
+
+ public @NonNull String getLabel() {
+ return mLabel;
+ }
+
+ public @StringRes int getListTitleRes() {
+ return mListTitleRes;
+ }
+
+ public @StringRes int getGraphTitleRes() {
+ return mGraphTitleRes;
+ }
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/handheld/ReviewOngoingUsageFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/ReviewOngoingUsageFragment.java
new file mode 100644
index 00000000..623d20f2
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/handheld/ReviewOngoingUsageFragment.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2019 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.ui.handheld;
+
+import static android.Manifest.permission_group.CAMERA;
+import static android.Manifest.permission_group.LOCATION;
+import static android.Manifest.permission_group.MICROPHONE;
+
+import static com.android.packageinstaller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED;
+import static com.android.packageinstaller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS;
+import static com.android.packageinstaller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM;
+import static com.android.packageinstaller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_PRIVACY_SETTINGS;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.android.packageinstaller.PermissionControllerStatsLog;
+import com.android.packageinstaller.permission.model.AppPermissionUsage;
+import com.android.packageinstaller.permission.model.AppPermissionUsage.GroupUsage;
+import com.android.packageinstaller.permission.model.PermissionApps;
+import com.android.packageinstaller.permission.model.PermissionApps.PermissionApp;
+import com.android.packageinstaller.permission.model.PermissionUsages;
+import com.android.packageinstaller.permission.utils.Utils;
+import com.android.permissioncontroller.R;
+
+import java.text.Collator;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A dialog listing the currently uses of camera, microphone, and location.
+ */
+public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat {
+
+ private @NonNull PermissionUsages mPermissionUsages;
+ private @Nullable AlertDialog mDialog;
+ private long mStartTime;
+
+ /**
+ * @return A new {@link ReviewOngoingUsageFragment}
+ */
+ public static ReviewOngoingUsageFragment newInstance(long numMillis) {
+ ReviewOngoingUsageFragment fragment = new ReviewOngoingUsageFragment();
+ Bundle arguments = new Bundle();
+ arguments.putLong(Intent.EXTRA_DURATION_MILLIS, numMillis);
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (!Utils.isPermissionsHubEnabled()) {
+ getActivity().finish();
+ return;
+ }
+
+ long numMillis = getArguments().getLong(Intent.EXTRA_DURATION_MILLIS);
+
+ mPermissionUsages = new PermissionUsages(getActivity());
+ mStartTime = Math.max(System.currentTimeMillis() - numMillis, Instant.EPOCH.toEpochMilli());
+ mPermissionUsages.load(null, new String[]{CAMERA, LOCATION, MICROPHONE}, mStartTime,
+ Long.MAX_VALUE, PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(),
+ false, false, this::onPermissionUsagesLoaded, false);
+ }
+
+ private void onPermissionUsagesLoaded() {
+ if (getActivity() == null) {
+ return;
+ }
+
+ List<AppPermissionUsage> appPermissionUsages = mPermissionUsages.getUsages();
+
+ List<Pair<AppPermissionUsage, List<GroupUsage>>> usages = new ArrayList<>();
+ ArrayList<PermissionApp> permApps = new ArrayList<>();
+ int numApps = appPermissionUsages.size();
+ for (int appNum = 0; appNum < numApps; appNum++) {
+ AppPermissionUsage appUsage = appPermissionUsages.get(appNum);
+
+ List<GroupUsage> usedGroups = new ArrayList<>();
+ List<GroupUsage> appGroups = appUsage.getGroupUsages();
+ int numGroups = appGroups.size();
+ for (int groupNum = 0; groupNum < numGroups; groupNum++) {
+ GroupUsage groupUsage = appGroups.get(groupNum);
+ String groupName = groupUsage.getGroup().getName();
+
+ if (groupUsage.getLastAccessTime() < mStartTime && !groupUsage.isRunning()) {
+ continue;
+ }
+ if (!Utils.isGroupOrBgGroupUserSensitive(groupUsage.getGroup())) {
+ continue;
+ }
+
+ usedGroups.add(appGroups.get(groupNum));
+ }
+
+ if (!usedGroups.isEmpty()) {
+ usages.add(Pair.create(appUsage, usedGroups));
+ permApps.add(appUsage.getApp());
+ }
+ }
+
+ if (usages.isEmpty()) {
+ getActivity().finish();
+ return;
+ }
+
+ new PermissionApps.AppDataLoader(getActivity(), () -> showDialog(usages))
+ .execute(permApps.toArray(new PermissionApps.PermissionApp[permApps.size()]));
+ }
+
+ private void showDialog(@NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+ .setView(createDialogView(usages))
+ .setPositiveButton(R.string.ongoing_usage_dialog_ok, (dialog, which) ->
+ PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED,
+ PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS, null))
+ .setOnDismissListener((dialog) -> getActivity().finish());
+ setNeutralButton(builder);
+ mDialog = builder.create();
+ mDialog.show();
+ }
+
+ protected void setNeutralButton(AlertDialog.Builder builder) {
+ builder.setNeutralButton(R.string.ongoing_usage_dialog_open_settings, (dialog, which) -> {
+ PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED,
+ PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_PRIVACY_SETTINGS, null);
+ startActivity(new Intent(Settings.ACTION_PRIVACY_SETTINGS).putExtra(
+ Intent.EXTRA_DURATION_MILLIS, TimeUnit.MINUTES.toMillis(1)));
+ });
+ }
+
+ private @NonNull View createDialogView(
+ @NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) {
+ Context context = getActivity();
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View contentView = inflater.inflate(R.layout.ongoing_usage_dialog_content, null);
+ ViewGroup appsList = contentView.requireViewById(R.id.items_container);
+
+ // Compute all of the permission group labels that were used.
+ ArraySet<String> usedGroups = new ArraySet<>();
+ int numUsages = usages.size();
+ for (int usageNum = 0; usageNum < numUsages; usageNum++) {
+ List<GroupUsage> groups = usages.get(usageNum).second;
+ int numGroups = groups.size();
+ for (int groupNum = 0; groupNum < numGroups; groupNum++) {
+ usedGroups.add(groups.get(groupNum).getGroup().getLabel().toString().toLowerCase());
+ }
+ }
+
+ // Add the layout for each app.
+ for (int usageNum = 0; usageNum < numUsages; usageNum++) {
+ Pair<AppPermissionUsage, List<GroupUsage>> usage = usages.get(usageNum);
+ PermissionApp app = usage.first.getApp();
+ List<GroupUsage> groups = usage.second;
+
+ View itemView = inflater.inflate(R.layout.ongoing_usage_dialog_item, appsList, false);
+
+ ((TextView) itemView.requireViewById(R.id.app_name)).setText(app.getLabel());
+ ((ImageView) itemView.requireViewById(R.id.app_icon)).setImageDrawable(app.getIcon());
+
+ // Add the icons for the groups this app used as long as multiple groups were used by
+ // some app.
+ if (usedGroups.size() > 1) {
+ ViewGroup iconFrame = itemView.requireViewById(R.id.icons);
+ int numGroups = usages.get(usageNum).second.size();
+ for (int groupNum = 0; groupNum < numGroups; groupNum++) {
+ ViewGroup group = (ViewGroup) inflater.inflate(R.layout.image_view, null);
+ ((ImageView) group.requireViewById(R.id.icon)).setImageDrawable(
+ Utils.applyTint(context, groups.get(groupNum).getGroup().getIconResId(),
+ android.R.attr.colorControlNormal));
+ iconFrame.addView(group);
+ }
+ iconFrame.setVisibility(View.VISIBLE);
+ }
+
+ itemView.setOnClickListener((v) -> {
+ String packageName = app.getPackageName();
+ PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED,
+ PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM, packageName);
+ UserHandle user = UserHandle.getUserHandleForUid(app.getUid());
+ Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
+ intent.putExtra(Intent.EXTRA_USER, user);
+ context.startActivity(intent);
+ mDialog.dismiss();
+ });
+
+ appsList.addView(itemView);
+ }
+
+ // Set the title of the dialog based on all of the permissions used.
+ StringBuilder titleBuilder = new StringBuilder();
+ int numGroups = usedGroups.size();
+ List<String> sortedGroups = new ArrayList<>(usedGroups);
+ Collator collator = Collator.getInstance(
+ getResources().getConfiguration().getLocales().get(0));
+ sortedGroups.sort(collator);
+ for (int i = 0; i < numGroups; i++) {
+ titleBuilder.append(sortedGroups.get(i));
+ if (i < numGroups - 2) {
+ titleBuilder.append(getString(R.string.ongoing_usage_dialog_separator));
+ } else if (i < numGroups - 1) {
+ titleBuilder.append(getString(R.string.ongoing_usage_dialog_last_separator));
+ }
+ }
+
+ ((TextView) contentView.requireViewById(R.id.title)).setText(
+ getString(R.string.ongoing_usage_dialog_title, titleBuilder.toString()));
+
+ return contentView;
+ }
+
+ @Override
+ public void onCreatePreferences(Bundle bundle, String s) {
+ // empty
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/utils/Utils.java b/src/com/android/packageinstaller/permission/utils/Utils.java
index 707b9f7b..867d603c 100644
--- a/src/com/android/packageinstaller/permission/utils/Utils.java
+++ b/src/com/android/packageinstaller/permission/utils/Utils.java
@@ -84,6 +84,7 @@ import com.android.launcher3.icons.IconFactory;
import com.android.packageinstaller.Constants;
import com.android.packageinstaller.permission.data.PerUserUidToSensitivityLiveData;
import com.android.packageinstaller.permission.model.AppPermissionGroup;
+import com.android.packageinstaller.permission.model.AppPermissionUsage;
import com.android.permissioncontroller.R;
import java.util.ArrayList;
@@ -101,6 +102,9 @@ public final class Utils {
public static final float DEFAULT_MAX_LABEL_SIZE_PX = 500f;
+ /** Whether to show the Permissions Hub. */
+ private static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_enabled";
+
/** Whether to show location access check notifications. */
private static final String PROPERTY_LOCATION_ACCESS_CHECK_ENABLED =
"location_access_check_enabled";
@@ -566,6 +570,39 @@ public final class Utils {
}
/**
+ * Build a string representing the amount of time passed since the most recent permission usage.
+ *
+ * @return a string representing the amount of time since this app's most recent permission
+ * usage or null if there are no usages.
+ */
+ public static @Nullable String getRelativeLastUsageString(@NonNull Context context,
+ @Nullable AppPermissionUsage.GroupUsage groupUsage) {
+ if (groupUsage == null || groupUsage.getLastAccessTime() == 0) {
+ return null;
+ }
+ return getTimeDiffStr(context, System.currentTimeMillis()
+ - groupUsage.getLastAccessTime());
+ }
+
+ /**
+ * Build a string representing the time of the most recent permission usage if it happened on
+ * the current day and the date otherwise.
+ *
+ * @param context the context.
+ * @param groupUsage the permission usage.
+ *
+ * @return a string representing the time or date of the most recent usage or null if there are
+ * no usages.
+ */
+ public static @Nullable String getAbsoluteLastUsageString(@NonNull Context context,
+ @Nullable AppPermissionUsage.GroupUsage groupUsage) {
+ if (groupUsage == null) {
+ return null;
+ }
+ return getAbsoluteTimeString(context, groupUsage.getLastAccessTime());
+ }
+
+ /**
* Build a string representing the given time if it happened on the current day and the date
* otherwise.
*
@@ -587,6 +624,20 @@ public final class Utils {
}
/**
+ * Build a string representing the duration of a permission usage.
+ *
+ * @return a string representing the duration of this app's usage or null if there are no
+ * usages.
+ */
+ public static @Nullable String getUsageDurationString(@NonNull Context context,
+ @Nullable AppPermissionUsage.GroupUsage groupUsage) {
+ if (groupUsage == null) {
+ return null;
+ }
+ return getTimeDiffStr(context, groupUsage.getAccessDuration());
+ }
+
+ /**
* 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".
@@ -727,6 +778,27 @@ public final class Utils {
}
/**
+ * Whether the Permissions Hub is enabled.
+ *
+ * @return whether the Permissions Hub is enabled.
+ */
+ public static boolean isPermissionsHubEnabled() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
+ PROPERTY_PERMISSIONS_HUB_ENABLED, false);
+ }
+
+ /**
+ * Whether we should show permission usages for the specified permission group.
+ *
+ * @param permissionGroup The name of the permission group.
+ *
+ * @return whether or not to show permission usages for the given permission group.
+ */
+ public static boolean shouldShowPermissionUsage(@NonNull String permissionGroup) {
+ return !permissionGroup.equals(STORAGE);
+ }
+
+ /**
* Get a device protected storage based shared preferences. Avoid storing sensitive data in it.
*
* @param context the context to get the shared preferences