blob: 0a06d2c13f70a3c4dcdce91f4916904896573b5c [file] [log] [blame]
/*
* 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 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.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.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Spinner;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceGroup;
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.PermissionUsages;
import com.android.packageinstaller.permission.utils.Utils;
import com.android.permissioncontroller.R;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.widget.BarChartInfo;
import com.android.settingslib.widget.BarChartPreference;
import com.android.settingslib.widget.BarViewInfo;
import com.android.settingslib.widget.settingsspinner.SettingsSpinnerAdapter;
import java.lang.annotation.Retention;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Calendar;
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 PermissionsFrameFragment implements
PermissionUsages.PermissionsUsagesChangeCallback, OnItemSelectedListener {
private static final String LOG_TAG = "PermissionUsageFragment";
@Retention(SOURCE)
@IntDef(value = {SORT_MOST_PERMISSIONS, SORT_MOST_ACCESSES, SORT_RECENT})
@interface SortOption {}
static final int SORT_MOST_PERMISSIONS = 1;
static final int SORT_MOST_ACCESSES = 2;
static final int SORT_RECENT = 3;
private static final int MENU_FILTER_BY_PERMISSIONS = MENU_HIDE_SYSTEM + 1;
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_PERMS_INDEX = "_time_index";
private static final String PERMS_INDEX_KEY = PermissionUsageFragment.class.getName()
+ KEY_PERMS_INDEX;
private static final String KEY_SPINNER_TIME_INDEX = "_time_index";
private static final String SPINNER_TIME_INDEX_KEY = PermissionUsageFragment.class.getName()
+ KEY_SPINNER_TIME_INDEX;
private static final String KEY_SPINNER_SORT_INDEX = "_sort_index";
private static final String SPINNER_SORT_INDEX_KEY = PermissionUsageFragment.class.getName()
+ KEY_SPINNER_SORT_INDEX;
/**
* The number of columns shown in the bar chart.
*/
private static final int BAR_CHART_COLUMN_COUNT = 4;
private @NonNull PermissionUsages mPermissionUsages;
private Collator mCollator;
private ArraySet<String> mLauncherPkgs;
private String mFilterGroup;
private boolean mShowSystem;
private boolean mHasSystemApps;
private MenuItem mShowSystemMenu;
private MenuItem mHideSystemMenu;
private Spinner mFilterSpinnerTime;
private FilterSpinnerAdapter<TimeFilterItem> mFilterAdapterTime;
private Spinner mSortSpinner;
private FilterSpinnerAdapter<SortItem> mSortAdapter;
/**
* Only used to restore permission selection state or use the passed permission after onCreate.
* Once the first list of groups is reported, this becomes invalid.
*/
private String mSavedGroupName;
/**
* Only used to restore time spinner state after onCreate. Once the list of times is reported,
* this becomes invalid.
*/
private int mSavedTimeSpinnerIndex;
/**
* Only used to restore sort spinner state after onCreate. Once the list of sorts is reported,
* this becomes invalid.
*/
private int mSavedSortSpinnerIndex;
/**
* @return A new fragment
*/
public static @NonNull PermissionUsageFragment newInstance(@Nullable String groupName) {
PermissionUsageFragment fragment = new PermissionUsageFragment();
Bundle arguments = new Bundle();
if (groupName != null) {
arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName);
}
fragment.setArguments(arguments);
return fragment;
}
@Override
public void onStart() {
super.onStart();
getActivity().setTitle(R.string.permission_usage_title);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mShowSystem = savedInstanceState.getBoolean(SHOW_SYSTEM_KEY);
mSavedGroupName = savedInstanceState.getString(PERMS_INDEX_KEY);
mSavedTimeSpinnerIndex = savedInstanceState.getInt(SPINNER_TIME_INDEX_KEY);
mSavedSortSpinnerIndex = savedInstanceState.getInt(SPINNER_SORT_INDEX_KEY);
}
setLoading(true, false);
setHasOptionsMenu(true);
ActionBar ab = getActivity().getActionBar();
if (ab != null) {
ab.setDisplayHomeAsUpEnabled(true);
}
if (mSavedGroupName == null) {
mSavedGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
}
Context context = getPreferenceManager().getContext();
mFilterGroup = null;
mCollator = Collator.getInstance(
context.getResources().getConfiguration().getLocales().get(0));
mLauncherPkgs = Utils.getLauncherPackages(context);
mPermissionUsages = new PermissionUsages(context);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Context context = getPreferenceManager().getContext();
ViewGroup root = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState);
// Setup filter spinners.
View header = inflater.inflate(R.layout.permission_usage_filter_spinners, root, false);
getPreferencesContainer().addView(header, 0);
mFilterSpinnerTime = header.requireViewById(R.id.filter_spinner_time);
mFilterAdapterTime = new FilterSpinnerAdapter<>(context);
mFilterSpinnerTime.setAdapter(mFilterAdapterTime);
mFilterSpinnerTime.setOnItemSelectedListener(this);
mSortSpinner = header.requireViewById(R.id.sort_spinner);
mSortAdapter = new FilterSpinnerAdapter<>(context);
mSortSpinner.setAdapter(mSortAdapter);
mSortSpinner.setOnItemSelectedListener(this);
// Add time spinner entries.
mFilterAdapterTime.addFilter(new TimeFilterItem(Long.MAX_VALUE,
context.getString(R.string.permission_usage_any_time),
R.string.permission_usage_bar_chart_title_any_time,
R.string.permission_usage_list_title_any_time));
mFilterAdapterTime.addFilter(new TimeFilterItem(DAYS.toMillis(7),
context.getString(R.string.permission_usage_last_7_days),
R.string.permission_usage_bar_chart_title_last_7_days,
R.string.permission_usage_list_title_last_7_days));
mFilterAdapterTime.addFilter(new TimeFilterItem(DAYS.toMillis(1),
context.getString(R.string.permission_usage_last_day),
R.string.permission_usage_bar_chart_title_last_day,
R.string.permission_usage_list_title_last_day));
mFilterAdapterTime.addFilter(new TimeFilterItem(HOURS.toMillis(1),
context.getString(R.string.permission_usage_last_hour),
R.string.permission_usage_bar_chart_title_last_hour,
R.string.permission_usage_list_title_last_hour));
mFilterAdapterTime.addFilter(new TimeFilterItem(MINUTES.toMillis(15),
context.getString(R.string.permission_usage_last_15_minutes),
R.string.permission_usage_bar_chart_title_last_15_minutes,
R.string.permission_usage_list_title_last_15_minutes));
mFilterSpinnerTime.setSelection(mSavedTimeSpinnerIndex);
// Add sort spinner entries.
mSortAdapter.addFilter(
new SortItem(context.getString(R.string.sort_spinner_most_permissions),
SORT_MOST_PERMISSIONS));
mSortAdapter.addFilter(new SortItem(context.getString(R.string.sort_spinner_most_accesses),
SORT_MOST_ACCESSES));
mSortAdapter.addFilter(new SortItem(context.getString(R.string.sort_spinner_recent),
SORT_RECENT));
mSortSpinner.setSelection(mSavedSortSpinnerIndex);
return root;
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (parent == mFilterSpinnerTime) {
reloadData();
} else if (parent == mSortSpinner) {
// We already loaded all data, so don't reload
updateUI();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(SHOW_SYSTEM_KEY, mShowSystem);
outState.putString(PERMS_INDEX_KEY, mFilterGroup);
outState.putInt(SPINNER_TIME_INDEX_KEY, mFilterSpinnerTime.getSelectedItemPosition());
outState.putInt(SPINNER_SORT_INDEX_KEY, mSortSpinner.getSelectedItemPosition());
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
menu.add(Menu.NONE, MENU_FILTER_BY_PERMISSIONS, Menu.NONE, R.string.filter_by_permissions);
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);
updateMenu();
}
HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_permission_usage,
getClass().getName());
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
getActivity().finish();
return true;
case MENU_FILTER_BY_PERMISSIONS:
showPermissionFilterDialog();
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;
}
return super.onOptionsItemSelected(item);
}
private void updateMenu() {
mShowSystemMenu.setVisible(!mShowSystem);
mHideSystemMenu.setVisible(mShowSystem);
}
@Override
public void onPermissionUsagesChanged() {
if (mPermissionUsages.getUsages().isEmpty()) {
return;
}
// Use the saved permission group or the one passed as an argument, if applicable.
if (mSavedGroupName != null && mFilterGroup == null) {
List<AppPermissionGroup> groups = getOSPermissionGroups();
int numGroups = groups.size();
for (int i = 0; i < numGroups; i++) {
if (groups.get(i).getName().equals(mSavedGroupName)) {
mFilterGroup = mSavedGroupName;
}
}
}
updateUI();
setLoading(false, true);
}
@Override
public int getEmptyViewString() {
return R.string.no_permission_usages;
}
private void updateUI() {
final List<AppPermissionUsage> appPermissionUsages =
new ArrayList<>(mPermissionUsages.getUsages());
if (appPermissionUsages.isEmpty() || getActivity() == null) {
return;
}
Context context = getActivity();
PreferenceScreen screen = getPreferenceScreen();
if (screen == null) {
screen = getPreferenceManager().createPreferenceScreen(context);
setPreferenceScreen(screen);
}
screen.removeAll();
// Update bar chart
final TimeFilterItem timeFilterItem = getSelectedFilterItem();
final BarChartPreference barChart = createBarChart(appPermissionUsages,
timeFilterItem, context);
screen.addPreference(barChart);
// Add the preferences.
PreferenceCategory category = new PreferenceCategory(context);
screen.addPreference(category);
if (timeFilterItem != null) {
category.setTitle(timeFilterItem.getListTitleRes());
}
// Sort the apps.
final int sortOption = getSelectedSortOption();
if (sortOption == SORT_MOST_PERMISSIONS) {
appPermissionUsages.sort(PermissionUsageFragment::compareAccessUsage);
} else if (sortOption == SORT_MOST_ACCESSES) {
appPermissionUsages.sort(PermissionUsageFragment::compareAccessCount);
} else if (sortOption == SORT_RECENT) {
appPermissionUsages.sort(PermissionUsageFragment::compareAccessRecency);
} else {
Log.w(LOG_TAG, "Unexpected sort option: " + sortOption);
}
mHasSystemApps = false;
final int numApps = appPermissionUsages.size();
for (int appNum = 0; appNum < numApps; appNum++) {
final AppPermissionUsage appPermissionUsage = appPermissionUsages.get(appNum);
if (appPermissionUsage.getAccessCount() <= 0) {
continue;
}
final boolean isSystemApp = Utils.isSystem(appPermissionUsage.getApp(),
mLauncherPkgs);
if (!mHasSystemApps) {
if (isSystemApp) {
mHasSystemApps = true;
getActivity().invalidateOptionsMenu();
}
}
if (isSystemApp && !mShowSystem) {
continue;
}
if (sortOption == SORT_MOST_ACCESSES) {
appPermissionUsage.getGroupUsages().sort(
PermissionUsageFragment::compareAccessCount);
} else {
appPermissionUsage.getGroupUsages().sort(
PermissionUsageFragment::compareAccessTime);
}
final List<GroupUsage> appGroups = appPermissionUsage.getGroupUsages();
final List<PermissionControlPreference> permissionPrefs = new ArrayList<>();
final int numGroups = appGroups.size();
for (int groupNum = 0; groupNum < numGroups; groupNum++) {
final GroupUsage groupUsage = appGroups.get(groupNum);
if (mFilterGroup != null && !mFilterGroup.equals(groupUsage.getGroup().getName())) {
continue;
}
if (groupUsage.getAccessCount() > 0) {
permissionPrefs.add(createPermissionUsagePreference(context,
appPermissionUsage, groupUsage, sortOption));
}
}
PreferenceGroup parent = category;
if (permissionPrefs.size() > 1) {
// Add a "parent" entry for the app that will expand to the individual entries.
parent = createExpandablePreferenceGroup(context, appPermissionUsage);
category.addPreference(parent);
}
final int permissionPrefCount = permissionPrefs.size();
for (int i = 0; i < permissionPrefCount; i++) {
final PermissionControlPreference permissionPref = permissionPrefs.get(i);
if (permissionPrefs.size() == 1) {
permissionPref.setIcon(appPermissionUsage.getApp().getIcon());
}
parent.addPreference(permissionPrefs.get(i));
}
}
}
private TimeFilterItem getSelectedFilterItem() {
// Get the current values of the time filter.
final int pos = mFilterSpinnerTime.getSelectedItemPosition();
TimeFilterItem timeFilterItem = null;
if (pos != AdapterView.INVALID_POSITION) {
timeFilterItem = mFilterAdapterTime.getFilter(pos);
}
return timeFilterItem;
}
private int getSelectedSortOption() {
final int pos = mSortSpinner.getSelectedItemPosition();
if (pos == AdapterView.INVALID_POSITION) {
return SORT_MOST_PERMISSIONS;
}
return mSortAdapter.getFilter(pos).getSortOption();
}
/**
* Reloads the data to show.
*/
private void reloadData() {
final TimeFilterItem timeFilterItem = getSelectedFilterItem();
if (timeFilterItem == null) {
return;
}
final long filterTimeBeginMillis = Math.max(System.currentTimeMillis()
- timeFilterItem.getTime(), Calendar.getInstance().getTimeInMillis());
mPermissionUsages.load(null /*filterPackageName*/, mFilterGroup,
filterTimeBeginMillis, Long.MAX_VALUE, PermissionUsages.USAGE_FLAG_LAST
| PermissionUsages.USAGE_FLAG_HISTORICAL, getActivity().getLoaderManager(),
true /*getUiInfo*/, this /*callback*/);
}
/**
* Create a bar chart showing the permissions that are used by the most apps.
*
* @param appPermissionUsages app permission 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<AppPermissionUsage> appPermissionUsages,
@Nullable TimeFilterItem timeFilterItem, @NonNull Context context) {
BarChartInfo.Builder builder = new BarChartInfo.Builder();
BarChartPreference barChart = new BarChartPreference(context, null);
if (timeFilterItem != null) {
builder.setTitle(timeFilterItem.getGraphTitleRes());
}
if (mFilterGroup != null) {
builder.setDetails(R.string.app_permission_usage_detail_label);
builder.setDetailsOnClickListener(v -> {
mFilterGroup = null;
reloadData();
});
}
final ArrayList<AppPermissionGroup> groups = new ArrayList<>();
final ArrayMap<String, Integer> groupToAppCount = new ArrayMap<>();
final int appCount = appPermissionUsages.size();
for (int i = 0; i < appCount; i++) {
final AppPermissionUsage appPermissionUsage = appPermissionUsages.get(i);
final List<AppPermissionUsage.GroupUsage> groupUsages =
appPermissionUsage.getGroupUsages();
final int groupCount = groupUsages.size();
for (int j = 0; j < groupCount; j++) {
final GroupUsage groupUsage = groupUsages.get(j);
if (groupUsage.getAccessCount() <= 0) {
continue;
}
final 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) -> {
final int usageDiff = compareLong(groupToAppCount.get(x.getName()),
groupToAppCount.get(y.getName()));
if (usageDiff != 0) {
return usageDiff;
}
// Make sure we lose no data if same
return y.hashCode() - x.hashCode();
});
for (int i = 0; i < BAR_CHART_COLUMN_COUNT; i++) {
final AppPermissionGroup group = groups.get(i);
final Drawable icon = Utils.loadDrawable(context.getPackageManager(),
group.getIconPkg(), group.getIconResId());
BarViewInfo barViewInfo = new BarViewInfo(
Utils.applyTint(context, icon, android.R.attr.colorControlNormal),
// The cast should not be a prob in practice
groupToAppCount.get(group.getName()),
R.string.app_permission_usage_bar_label);
barViewInfo.setClickListener(v -> {
mFilterGroup = group.getName();
reloadData();
});
builder.addBarViewInfo(barViewInfo);
}
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 PreferenceGroup createExpandablePreferenceGroup(@NonNull Context context,
@NonNull AppPermissionUsage appPermissionUsage) {
final List<GroupUsage> groupUsages = appPermissionUsage.getGroupUsages();
final List<Integer> permissionIcons = new ArrayList<>(groupUsages.size());
final int permissionUsageCount = groupUsages.size();
for (int i = 0; i < permissionUsageCount; i++) {
final AppPermissionUsage.GroupUsage groupUsage = groupUsages.get(i);
if (groupUsage.getAccessCount() > 0) {
permissionIcons.add(groupUsage.getGroup().getIconResId());
}
}
PreferenceGroup preference = new ExpandablePreferenceGroup(context, permissionIcons);
preference.setTitle(appPermissionUsage.getApp().getLabel());
preference.setIcon(appPermissionUsage.getApp().getIcon());
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 sortOption how the entries should be sorted
*
* @return the Preference
*/
private PermissionControlPreference createPermissionUsagePreference(@NonNull Context context,
@NonNull AppPermissionUsage appPermissionUsage,
@NonNull GroupUsage groupUsage, @SortOption int sortOption) {
final PermissionControlPreference pref = new PermissionControlPreference(context,
groupUsage.getGroup());
pref.setTitle(appPermissionUsage.getApp().getLabel());
final AppPermissionGroup group = groupUsage.getGroup();
if (sortOption == SORT_MOST_ACCESSES) {
if (groupUsage.getBackgroundAccessCount() == 0) {
pref.setSummary(
context.getString(R.string.permission_usage_summary_num_accesses,
group.getLabel(), groupUsage.getForegroundAccessCount()));
} else {
pref.setSummary(
context.getString(
R.string.permission_usage_summary_num_accesses_background,
group.getLabel(), groupUsage.getAccessCount(),
groupUsage.getBackgroundAccessCount()));
}
} else {
pref.setSummary(context.getString(R.string.permission_usage_summary_last_access,
group.getLabel(), Utils.getTimeDiffStr(context, System.currentTimeMillis()
- groupUsage.getLastAccessTime())));
}
pref.setSummaryIcons(Collections.singletonList(group.getIconResId()));
pref.setKey(group.getApp().packageName + "," + group.getName());
pref.useSmallerIcon();
return pref;
}
/**
* Compare two AppPermissionUsage by their permission usage.
*
* 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 compareAccessUsage(@NonNull AppPermissionUsage x,
@NonNull AppPermissionUsage y) {
final int groupDiff = getAccessedGroupCount(y) - getAccessedGroupCount(x);
if (groupDiff != 0) {
return groupDiff;
}
return compareAccessTime(x, y);
}
/**
* Gets the number of permission groups that have been accessed by the given app.
*
* @param appPermissionUsage The app permission usage.
* @return The access count.
*/
private static int getAccessedGroupCount(@NonNull AppPermissionUsage appPermissionUsage) {
int accessedCount = 0;
final List<GroupUsage> groupUsages = appPermissionUsage.getGroupUsages();
final int groupCount = groupUsages.size();
for (int i = 0; i < groupCount; i++) {
final GroupUsage groupUsage = groupUsages.get(i);
if (groupUsage.getAccessCount() > 0) {
accessedCount++;
}
}
return accessedCount;
}
/**
* 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.GroupUsage x,
@NonNull AppPermissionUsage.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 AppPermissionUsage by their access count.
*
* 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 compareAccessCount(@NonNull AppPermissionUsage x,
@NonNull AppPermissionUsage y) {
final int accessDiff = compareLong(x.getAccessCount(), y.getAccessCount());
if (accessDiff != 0) {
return accessDiff;
}
// Make sure we lose no data if same
return y.hashCode() - x.hashCode();
}
/**
* Compare two AppPermissionUsage by their access count.
*
* 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 compareAccessCount(@NonNull AppPermissionUsage.GroupUsage x,
@NonNull GroupUsage y) {
final int accessDiff = compareLong(x.getAccessCount(), y.getAccessCount());
if (accessDiff != 0) {
return accessDiff;
}
// Make sure we lose no data if same
return y.hashCode() - x.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 AppPermissionUsage by recency of access.
*
* 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 compareAccessRecency(@NonNull AppPermissionUsage x,
@NonNull AppPermissionUsage y) {
final int timeDiff = compareAccessTime(x, y);
if (timeDiff != 0) {
return timeDiff;
}
final int countDiff = y.getGroupUsages().size() - x.getGroupUsages().size();
if (countDiff != 0) {
return countDiff;
}
// 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 List<AppPermissionUsage> appUsages = mPermissionUsages.getUsages();
final int numGroups = appUsages.size();
for (int i = 0; i < numGroups; i++) {
final AppPermissionUsage appUsage = appUsages.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;
}
/**
* 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 spinner entries.
String[] groupNames = new String[groups.size() + 1];
CharSequence[] groupLabels = new CharSequence[groupNames.length];
groupNames[0] = null;
groupLabels[0] = context.getString(R.string.permission_usage_any_permission);
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();
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);
PermissionsFilterDialog chooserDialog = new PermissionsFilterDialog(this, groupNames);
chooserDialog.setArguments(args);
chooserDialog.show(getChildFragmentManager().beginTransaction(), "backgroundChooser");
}
/**
* 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) {
mFilterGroup = selectedGroup;
// We already loaded all data, so don't reload
updateUI();
}
/**
* 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 @NonNull PermissionUsageFragment mFragment;
private @NonNull String[] mGroups;
public PermissionsFilterDialog(@NonNull PermissionUsageFragment fragment,
@NonNull String[] groups) {
mFragment = fragment;
mGroups = groups;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
CharSequence[] elems = getArguments().getCharSequenceArray(ELEMS);
AlertDialog.Builder b = new AlertDialog.Builder(getActivity())
.setTitle(getArguments().getCharSequence(TITLE))
.setSingleChoiceItems(elems, getArguments().getInt(SELECTION),
(dialog, which) -> {
dismissAllowingStateLoss();
mFragment.onPermissionGroupSelected(mGroups[which]);
}
);
return b.create();
}
}
/**
* An adapter that stores the entries in a filter spinner.
* @param <T> The type of the entries in the filter spinner.
*/
private static class FilterSpinnerAdapter<T extends SpinnerItem> extends
SettingsSpinnerAdapter<CharSequence> {
private final ArrayList<T> mFilterOptions = new ArrayList<>();
FilterSpinnerAdapter(@NonNull Context context) {
super(context);
}
public void addFilter(@NonNull T filter) {
mFilterOptions.add(filter);
notifyDataSetChanged();
}
public T getFilter(int position) {
return mFilterOptions.get(position);
}
@Override
public int getCount() {
return mFilterOptions.size();
}
@Override
public CharSequence getItem(int position) {
return mFilterOptions.get(position).getLabel();
}
@Override
public void clear() {
mFilterOptions.clear();
super.clear();
}
}
/**
* An interface to represent items that we can use as filters.
*/
private interface SpinnerItem {
@NonNull String getLabel();
}
/**
* A spinner item representing a given time, e.g., "in the last hour".
*/
private static class TimeFilterItem implements SpinnerItem {
private final long mTime;
private final @NonNull String mLabel;
private final @StringRes int mGraphTitleRes;
private final @StringRes int mListTitleRes;
TimeFilterItem(long time, @NonNull String label, @StringRes int graphTitleRes,
@StringRes int listTitleRes) {
mTime = time;
mLabel = label;
mGraphTitleRes = graphTitleRes;
mListTitleRes = listTitleRes;
}
/**
* 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 getGraphTitleRes() {
return mGraphTitleRes;
}
public @StringRes int getListTitleRes() {
return mListTitleRes;
}
}
/**
* A spinner item representing different ways to sort the entries.
*/
private static class SortItem implements SpinnerItem {
private final @NonNull String mLabel;
private final @SortOption int mSortOption;
SortItem(@NonNull String label, @SortOption int sortOption) {
mLabel = label;
mSortOption = sortOption;
}
public @NonNull String getLabel() {
return mLabel;
}
public @SortOption int getSortOption() {
return mSortOption;
}
}
}