/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.permissioncontroller.permission.debug; 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.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; 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.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.model.AppPermissionUsage; import com.android.permissioncontroller.permission.model.AppPermissionUsage.GroupUsage; import com.android.permissioncontroller.permission.model.AppPermissionGroup; import com.android.permissioncontroller.permission.model.legacy.PermissionApps; import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp; import com.android.permissioncontroller.permission.ui.handheld.PermissionControlPreference; import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader; import com.android.permissioncontroller.permission.utils.Utils; 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. * *

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 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 mAppPermissionUsages = new ArrayList<>(); private Collator mCollator; private @NonNull List 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 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); } 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; } return super.onOptionsItemSelected(item); } private void updateMenu() { if (mHasSystemApps) { /* Do not show system apps for now mShowSystemMenu.setVisible(!mShowSystem); mHideSystemMenu.setVisible(mShowSystem); */ mShowSystemMenu.setVisible(false); mHideSystemMenu.setVisible(false); } mSortByApp.setVisible(mSort != SORT_RECENT_APPS); mSortByTime.setVisible(mSort != SORT_RECENT); } @Override public void onPermissionUsagesChanged() { 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> usages = new ArrayList<>(); mGroupAppCounts.clear(); ArrayList permApps = new ArrayList<>(); int numApps = mAppPermissionUsages.size(); for (int appNum = 0; appNum < numApps; appNum++) { AppPermissionUsage appUsage = mAppPermissionUsages.get(appNum); boolean used = false; List appGroups = appUsage.getGroupUsages(); int numGroups = appGroups.size(); for (int groupNum = 0; groupNum < numGroups; groupNum++) { GroupUsage groupUsage = appGroups.get(groupNum); long lastAccessTime = groupUsage.getLastAccessTime(); if (groupUsage.getAccessCount() <= 0) { continue; } 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 groups = new ArrayList<>(); final int numUsages = usages.size(); for (int usageNum = 0; usageNum < numUsages; usageNum++) { final Pair usage = usages.get(usageNum); AppPermissionUsage appPermissionUsage = usage.first; GroupUsage groupUsage = usage.second; String accessTimeString = UtilsKt.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 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> usages, @Nullable TimeFilterItem timeFilterItem, @NonNull Context context) { ArrayList groups = new ArrayList<>(); ArrayMap groupToAppCount = new ArrayMap<>(); int usageCount = usages.size(); for (int i = 0; i < usageCount; i++) { Pair 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 x, @NonNull Pair 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 x, @NonNull Pair 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 x, @NonNull Pair 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 getOSPermissionGroups() { final List groups = new ArrayList<>(); final Set seenGroups = new ArraySet<>(); final int numGroups = mAppPermissionUsages.size(); for (int i = 0; i < numGroups; i++) { final AppPermissionUsage appUsage = mAppPermissionUsages.get(i); final List 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 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 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; } } }