diff options
Diffstat (limited to 'src/com/android')
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 d846ce09..57f39927 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; @@ -495,7 +498,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 548545b5..4cfab87e 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 |