diff options
Diffstat (limited to 'src/com/android/packageinstaller/permission/ui/handheld/PermissionUsageFragment.java')
-rw-r--r-- | src/com/android/packageinstaller/permission/ui/handheld/PermissionUsageFragment.java | 1056 |
1 files changed, 1056 insertions, 0 deletions
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; + } + } +} |