blob: 14de26330cfc8170ad8d5b64c3edbb2a90f67480 [file] [log] [blame]
/*
* Copyright (C) 2021 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 java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import android.Manifest.permission_group;
import android.app.ActionBar;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.util.ArraySet;
import android.util.Pair;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.permissioncontroller.R;
import com.android.permissioncontroller.permission.model.AppPermissionGroup;
import com.android.permissioncontroller.permission.model.AppPermissionUsage;
import com.android.permissioncontroller.permission.model.legacy.PermissionApps;
import com.android.permissioncontroller.permission.ui.handheld.PermissionGroupPreference;
import com.android.permissioncontroller.permission.ui.handheld.PermissionHistoryPreference;
import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader;
import com.android.permissioncontroller.permission.utils.Utils;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* The permission details page showing the history/timeline of a permission
*/
public class PermissionDetailsFragment extends SettingsWithLargeHeader implements
PermissionUsages.PermissionsUsagesChangeCallback {
public static final int FILTER_24_HOURS = 2;
private static final List<String> ALLOW_CLUSTERING_PERMISSION_GROUPS = Arrays.asList(
permission_group.LOCATION, permission_group.CAMERA, permission_group.MICROPHONE
);
private static final int ONE_HOUR_MS = 3600000;
private static final int ONE_MINUTE_MS = 60000;
private static final int CLUSTER_MINUTES_APART = 1;
private @Nullable String mFilterGroup;
private @Nullable List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>();
private @NonNull List<TimeFilterItem> mFilterTimes;
private int mFilterTimeIndex;
private @NonNull PermissionUsages mPermissionUsages;
private boolean mFinishedInitialLoad;
/**
* Construct a new instance of PermissionDetailsFragment
*/
public static @NonNull PermissionDetailsFragment newInstance(@Nullable String groupName,
long numMillis) {
PermissionDetailsFragment fragment = new PermissionDetailsFragment();
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;
initializeTimeFilter();
mFilterTimeIndex = FILTER_24_HOURS;
if (mFilterGroup == null) {
mFilterGroup = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
}
setHasOptionsMenu(true);
ActionBar ab = getActivity().getActionBar();
if (ab != null) {
ab.setDisplayHomeAsUpEnabled(true);
}
Context context = getPreferenceManager().getContext();
mPermissionUsages = new PermissionUsages(context);
reloadData();
}
@Override
public void onStart() {
super.onStart();
getActivity().setTitle(R.string.permission_history_title);
}
@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 boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
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();
final TimeFilterItem timeFilterItem = mFilterTimes.get(mFilterTimeIndex);
long curTime = System.currentTimeMillis();
long startTime = Math.max(timeFilterItem == null ? 0 : (curTime - timeFilterItem.getTime()),
0);
PermissionGroupPreference permissionPreference = new PermissionGroupPreference(context,
getResources(), mFilterGroup);
screen.addPreference(permissionPreference);
ArrayList<PermissionApps.PermissionApp> permApps = new ArrayList<>();
List<AppPermissionUsageEntry> usages = mAppPermissionUsages.stream().map(appUsage -> {
// Fetch the access time list of the app accesses mFilterGroup permission group
// The DiscreteAccessTime is a Pair of (access time, access duration) of that app
List<Pair<Long, Long>> discreteAccessTimeList = appUsage.getGroupUsages().stream()
.filter(
groupUsage -> groupUsage.getGroup().getName().equals(mFilterGroup)
&& groupUsage.hasDiscreteData())
.flatMap(groupUsage -> groupUsage.getAllDiscreteAccessTime().stream())
.filter(discreteAccessTime -> discreteAccessTime.first != 0
&& discreteAccessTime.first >= startTime)
.sorted((x, y) -> y.first.compareTo(x.first))
.collect(Collectors.toList());
if (discreteAccessTimeList.size() > 0) {
permApps.add(appUsage.getApp());
}
// If the current permission group is not LOCATION or there's only one access for
// the app, return individual entry early.
if (!ALLOW_CLUSTERING_PERMISSION_GROUPS.contains(mFilterGroup)
|| discreteAccessTimeList.size() <= 1) {
return discreteAccessTimeList.stream().map(
time -> new AppPermissionUsageEntry(appUsage, time.first,
Collections.singletonList(time))).collect(Collectors.toList());
}
// Group access time list
List<AppPermissionUsageEntry> usageEntries = new ArrayList<>();
AppPermissionUsageEntry ongoingEntry = null;
for (Pair<Long, Long> time : discreteAccessTimeList) {
if (ongoingEntry == null) {
ongoingEntry = new AppPermissionUsageEntry(appUsage, time.first,
Stream.of(time).collect(Collectors.toCollection(ArrayList::new)));
} else {
List<Pair<Long, Long>> ongoingAccessTimeList =
ongoingEntry.mClusteredAccessTimeList;
if (time.first / ONE_HOUR_MS
!= ongoingAccessTimeList.get(0).first / ONE_HOUR_MS
|| ongoingAccessTimeList.get(ongoingAccessTimeList.size() - 1).first
/ ONE_MINUTE_MS - time.first / ONE_MINUTE_MS
> CLUSTER_MINUTES_APART) {
// If the current access time is not in the same hour nor within
// CLUSTER_MINUTES_APART, add the ongoing entry to the usage list and start
// a new ongoing entry.
usageEntries.add(ongoingEntry);
ongoingEntry = new AppPermissionUsageEntry(appUsage, time.first,
Stream.of(time).collect(Collectors.toCollection(ArrayList::new)));
} else {
ongoingAccessTimeList.add(time);
ongoingEntry.mStartTime = time.first;
}
}
}
usageEntries.add(ongoingEntry);
return usageEntries;
}).flatMap(Collection::stream).sorted((x, y) -> {
// Sort all usage entries by startTime desc, and then by app name.
int timeCompare = Long.compare(y.mStartTime, x.mStartTime);
if (timeCompare != 0) {
return timeCompare;
}
return x.mAppPermissionUsage.getApp().getLabel().compareTo(
y.mAppPermissionUsage.getApp().getLabel());
}).collect(Collectors.toList());
long midnightToday = Instant.now().truncatedTo(ChronoUnit.DAYS).toEpochMilli();
AppPermissionUsageEntry midnightTodayEntry = new AppPermissionUsageEntry(
null, midnightToday, null);
// Use the placeholder pair midnightTodayPair to get
// the index of the first usage entry from yesterday
int todayCategoryIndex = 0;
int yesterdayCategoryIndex = Collections.binarySearch(usages,
midnightTodayEntry, (e1, e2) -> Long.compare(e2.getStartTime(), e1.getStartTime()));
if (yesterdayCategoryIndex < 0) {
yesterdayCategoryIndex = -1 * (yesterdayCategoryIndex + 1);
}
// Make these variables effectively final so that
// we can use these captured variables in the below lambda expression
AtomicReference<PreferenceCategory> category = new AtomicReference<>(
new PreferenceCategory(context));
screen.addPreference(category.get());
PreferenceScreen finalScreen = screen;
int finalYesterdayCategoryIndex = yesterdayCategoryIndex;
new PermissionApps.AppDataLoader(context, () -> {
final int numUsages = usages.size();
for (int usageNum = 0; usageNum < numUsages; usageNum++) {
AppPermissionUsageEntry usage = usages.get(usageNum);
if (finalYesterdayCategoryIndex == usageNum) {
if (finalYesterdayCategoryIndex != todayCategoryIndex) {
// We create a new category only when we need it.
// We will not create a new category if we only need one category for
// either today's or yesterday's usage
category.set(new PreferenceCategory(context));
finalScreen.addPreference(category.get());
}
category.get().setTitle(R.string.permission_history_category_yesterday);
} else if (todayCategoryIndex == usageNum) {
category.get().setTitle(R.string.permission_history_category_today);
}
String accessTime = DateFormat.getTimeFormat(context).format(usage.mStartTime);
Long accessDurationLong = usage.mClusteredAccessTimeList
.stream()
.map(p -> p.second)
.filter(dur -> dur > 0)
.reduce(0L, (dur1, dur2) -> dur1 + dur2);
String accessDuration = null;
if (accessDurationLong > 0) {
accessDuration = UtilsKt.getDurationUsedStr(context, accessDurationLong);
}
PermissionHistoryPreference permissionUsagePreference = new
PermissionHistoryPreference(context,
usage.mAppPermissionUsage.getPackageName(),
mFilterGroup, accessTime, usage.mAppPermissionUsage.getApp().getIcon(),
usage.mAppPermissionUsage.getApp().getLabel(), accessDuration);
if (usage.mClusteredAccessTimeList.size() > 1) {
permissionUsagePreference.setAccessTimeList(usage.mClusteredAccessTimeList
.stream().map(p -> p.first).collect(Collectors.toList()));
ArrayList<String> attributionTags =
usage.mAppPermissionUsage.getGroupUsages().stream().filter(groupUsage ->
groupUsage.getGroup().getName().equals(mFilterGroup)).map(
AppPermissionUsage.GroupUsage::getAttributionTags).filter(
Objects::nonNull).flatMap(Collection::stream).collect(
Collectors.toCollection(ArrayList::new));
permissionUsagePreference.setAttributionTags(attributionTags);
}
category.get().addPreference(permissionUsagePreference);
}
setLoading(false, true);
mFinishedInitialLoad = true;
setProgressBarVisible(false);
mPermissionUsages.stopLoader(getActivity().getLoaderManager());
}).execute(permApps.toArray(new PermissionApps.PermissionApp[permApps.size()]));
}
/**
* 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;
}
/**
* Get the permission groups declared by the OS.
*
* TODO: theianchen change the method name to make that clear,
* and return a list of string group names, not AppPermissionGroups.
* @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<AppPermissionUsage.GroupUsage> groupUsages = appUsage.getGroupUsages();
final int groupUsageCount = groupUsages.size();
for (int j = 0; j < groupUsageCount; j++) {
final AppPermissionUsage.GroupUsage groupUsage = groupUsages.get(j);
if (Utils.isModernPermissionGroup(groupUsage.getGroup().getName())) {
if (seenGroups.add(groupUsage.getGroup().getName())) {
groups.add(groupUsage.getGroup());
}
}
}
}
return groups;
}
private void reloadData() {
final TimeFilterItem timeFilterItem = mFilterTimes.get(mFilterTimeIndex);
final long filterTimeBeginMillis = Math.max(System.currentTimeMillis()
- timeFilterItem.getTime(), 0);
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);
}
}
/**
* 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)));
mFilterTimes.add(new TimeFilterItem(DAYS.toMillis(7),
context.getString(R.string.permission_usage_last_7_days)));
mFilterTimes.add(new TimeFilterItem(DAYS.toMillis(1),
context.getString(R.string.permission_usage_last_day)));
mFilterTimes.add(new TimeFilterItem(HOURS.toMillis(1),
context.getString(R.string.permission_usage_last_hour)));
mFilterTimes.add(new TimeFilterItem(MINUTES.toMillis(15),
context.getString(R.string.permission_usage_last_15_minutes)));
mFilterTimes.add(new TimeFilterItem(MINUTES.toMillis(1),
context.getString(R.string.permission_usage_last_minute)));
// TODO: theianchen add code for filtering by time here.
}
/**
* 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;
TimeFilterItem(long time, @NonNull String label) {
mTime = time;
mLabel = label;
}
/**
* 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;
}
}
/**
* A class representing an app usage entry in Permission Usage.
*/
private static class AppPermissionUsageEntry {
private final AppPermissionUsage mAppPermissionUsage;
private final List<Pair<Long, Long>> mClusteredAccessTimeList;
private long mStartTime;
AppPermissionUsageEntry(AppPermissionUsage appPermissionUsage, long startTime,
List<Pair<Long, Long>> clusteredAccessTimeList) {
mAppPermissionUsage = appPermissionUsage;
mStartTime = startTime;
mClusteredAccessTimeList = clusteredAccessTimeList;
}
public AppPermissionUsage getAppPermissionUsage() {
return mAppPermissionUsage;
}
public long getStartTime() {
return mStartTime;
}
public List<Pair<Long, Long>> getAccessTime() {
return mClusteredAccessTimeList;
}
}
}