1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.permissioncontroller.permission.ui.handheld.dashboard;
18 
19 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID;
20 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID;
21 
22 import static java.util.concurrent.TimeUnit.DAYS;
23 import static java.util.concurrent.TimeUnit.HOURS;
24 import static java.util.concurrent.TimeUnit.MINUTES;
25 
26 import android.Manifest.permission_group;
27 import android.app.ActionBar;
28 import android.app.Activity;
29 import android.app.AppOpsManager.OpEventProxyInfo;
30 import android.app.role.RoleManager;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.res.ColorStateList;
34 import android.content.res.Configuration;
35 import android.content.res.TypedArray;
36 import android.os.Build;
37 import android.os.Bundle;
38 import android.os.UserHandle;
39 import android.text.format.DateFormat;
40 import android.util.ArraySet;
41 import android.view.LayoutInflater;
42 import android.view.Menu;
43 import android.view.MenuInflater;
44 import android.view.MenuItem;
45 import android.view.View;
46 import android.view.ViewGroup;
47 
48 import androidx.annotation.NonNull;
49 import androidx.annotation.Nullable;
50 import androidx.annotation.RequiresApi;
51 import androidx.coordinatorlayout.widget.CoordinatorLayout;
52 import androidx.preference.Preference;
53 import androidx.preference.PreferenceCategory;
54 import androidx.preference.PreferenceScreen;
55 import androidx.recyclerview.widget.RecyclerView;
56 
57 import com.android.permissioncontroller.PermissionControllerApplication;
58 import com.android.permissioncontroller.R;
59 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
60 import com.android.permissioncontroller.permission.model.AppPermissionUsage;
61 import com.android.permissioncontroller.permission.model.PermissionUsages;
62 import com.android.permissioncontroller.permission.model.legacy.PermissionApps;
63 import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity;
64 import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader;
65 import com.android.permissioncontroller.permission.utils.KotlinUtils;
66 import com.android.permissioncontroller.permission.utils.Utils;
67 
68 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
69 
70 import java.time.ZonedDateTime;
71 import java.time.temporal.ChronoUnit;
72 import java.util.ArrayList;
73 import java.util.Arrays;
74 import java.util.Collection;
75 import java.util.Collections;
76 import java.util.List;
77 import java.util.Objects;
78 import java.util.Set;
79 import java.util.concurrent.atomic.AtomicBoolean;
80 import java.util.concurrent.atomic.AtomicReference;
81 import java.util.stream.Collectors;
82 import java.util.stream.Stream;
83 
84 import kotlin.Triple;
85 
86 /**
87  * The permission details page showing the history/timeline of a permission
88  */
89 @RequiresApi(Build.VERSION_CODES.S)
90 public class PermissionDetailsFragment extends SettingsWithLargeHeader implements
91         PermissionUsages.PermissionsUsagesChangeCallback {
92     public static final int FILTER_24_HOURS = 2;
93 
94     private static final List<String> ALLOW_CLUSTERING_PERMISSION_GROUPS = Arrays.asList(
95             permission_group.LOCATION, permission_group.CAMERA, permission_group.MICROPHONE
96     );
97     private static final int ONE_HOUR_MS = 3600000;
98     private static final int ONE_MINUTE_MS = 60000;
99     private static final int CLUSTER_MINUTES_APART = 1;
100 
101     private static final String KEY_SHOW_SYSTEM_PREFS = "_show_system";
102     private static final String SHOW_SYSTEM_KEY = PermissionDetailsFragment.class.getName()
103             + KEY_SHOW_SYSTEM_PREFS;
104 
105     private static final String KEY_SESSION_ID = "_session_id";
106     private static final String SESSION_ID_KEY = PermissionDetailsFragment.class.getName()
107             + KEY_SESSION_ID;
108 
109     private @Nullable String mFilterGroup;
110     private @Nullable List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>();
111     private @NonNull List<TimeFilterItem> mFilterTimes;
112     private int mFilterTimeIndex;
113     private @NonNull PermissionUsages mPermissionUsages;
114     private boolean mFinishedInitialLoad;
115 
116     private boolean mShowSystem;
117     private boolean mHasSystemApps;
118 
119     private MenuItem mShowSystemMenu;
120     private MenuItem mHideSystemMenu;
121     private @NonNull RoleManager mRoleManager;
122 
123     private long mSessionId;
124 
125     @Override
onCreate(Bundle savedInstanceState)126     public void onCreate(Bundle savedInstanceState) {
127         super.onCreate(savedInstanceState);
128 
129         mFinishedInitialLoad = false;
130         initializeTimeFilter();
131         mFilterTimeIndex = FILTER_24_HOURS;
132 
133         if (savedInstanceState != null) {
134             mShowSystem = savedInstanceState.getBoolean(SHOW_SYSTEM_KEY);
135             mSessionId = savedInstanceState.getLong(SESSION_ID_KEY);
136         } else {
137             mShowSystem = getArguments().getBoolean(
138                     ManagePermissionsActivity.EXTRA_SHOW_SYSTEM, false);
139             mSessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
140         }
141 
142         if (mFilterGroup == null) {
143             mFilterGroup = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
144         }
145 
146         setHasOptionsMenu(true);
147         ActionBar ab = getActivity().getActionBar();
148         if (ab != null) {
149             ab.setDisplayHomeAsUpEnabled(true);
150         }
151 
152         Context context = getPreferenceManager().getContext();
153 
154         mPermissionUsages = new PermissionUsages(context);
155         mRoleManager = Utils.getSystemServiceSafe(context, RoleManager.class);
156 
157         reloadData();
158     }
159 
160     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)161     public View onCreateView(LayoutInflater inflater, ViewGroup container,
162             Bundle savedInstanceState) {
163         ViewGroup rootView = (ViewGroup) super.onCreateView(inflater, container,
164                 savedInstanceState);
165 
166         PermissionDetailsWrapperFragment parentFragment = (PermissionDetailsWrapperFragment)
167                 requireParentFragment();
168         CoordinatorLayout coordinatorLayout = parentFragment.getCoordinatorLayout();
169         inflater.inflate(R.layout.permission_details_extended_fab, coordinatorLayout);
170         ExtendedFloatingActionButton extendedFab = coordinatorLayout.requireViewById(
171                 R.id.extended_fab);
172         // Load the background tint color from the application theme
173         // rather than the Material Design theme
174         Activity activity = getActivity();
175         ColorStateList backgroundColor = activity.getColorStateList(
176                 android.R.color.system_accent3_100);
177         extendedFab.setBackgroundTintList(backgroundColor);
178         extendedFab.setText(R.string.manage_permission);
179         boolean isUiModeNight = (activity.getResources().getConfiguration().uiMode
180                 & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
181         int textColorAttr = isUiModeNight ? android.R.attr.textColorPrimaryInverse
182                 : android.R.attr.textColorPrimary;
183         TypedArray typedArray = activity.obtainStyledAttributes(new int[] { textColorAttr });
184         ColorStateList textColor = typedArray.getColorStateList(0);
185         typedArray.recycle();
186         extendedFab.setTextColor(textColor);
187         extendedFab.setIcon(activity.getDrawable(R.drawable.ic_settings_outline));
188         extendedFab.setVisibility(View.VISIBLE);
189         extendedFab.setOnClickListener(view -> {
190             Intent intent = new Intent(Intent.ACTION_MANAGE_PERMISSION_APPS)
191                     .putExtra(Intent.EXTRA_PERMISSION_NAME, mFilterGroup);
192             startActivity(intent);
193         });
194         RecyclerView recyclerView = getListView();
195         int bottomPadding = getResources()
196                 .getDimensionPixelSize(R.dimen.privhub_details_recycler_view_bottom_padding);
197         recyclerView.setPadding(0, 0, 0, bottomPadding);
198         recyclerView.setClipToPadding(false);
199         recyclerView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
200 
201         return rootView;
202     }
203 
204     @Override
onStart()205     public void onStart() {
206         super.onStart();
207         CharSequence title = getString(R.string.permission_history_title);
208         if (mFilterGroup != null) {
209             title = getResources().getString(R.string.permission_group_usage_title,
210                     KotlinUtils.INSTANCE.getPermGroupLabel(getActivity(), mFilterGroup));
211         }
212         getActivity().setTitle(title);
213     }
214 
215     @Override
onPermissionUsagesChanged()216     public void onPermissionUsagesChanged() {
217         if (mPermissionUsages.getUsages().isEmpty()) {
218             return;
219         }
220         mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages());
221 
222         // Ensure the group name is valid.
223         if (getGroup(mFilterGroup) == null) {
224             mFilterGroup = null;
225         }
226 
227         updateUI();
228     }
229 
230     @Override
onSaveInstanceState(Bundle outState)231     public void onSaveInstanceState(Bundle outState) {
232         super.onSaveInstanceState(outState);
233         outState.putBoolean(SHOW_SYSTEM_KEY, mShowSystem);
234         outState.putLong(SESSION_ID_KEY, mSessionId);
235     }
236 
237     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)238     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
239         mShowSystemMenu = menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE,
240                 R.string.menu_show_system);
241         mHideSystemMenu = menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE,
242                 R.string.menu_hide_system);
243 
244         updateMenu();
245     }
246 
updateMenu()247     private void updateMenu() {
248         if (mHasSystemApps) {
249             mShowSystemMenu.setVisible(!mShowSystem);
250             mShowSystemMenu.setEnabled(true);
251 
252             mHideSystemMenu.setVisible(mShowSystem);
253             mHideSystemMenu.setEnabled(true);
254         } else {
255             mShowSystemMenu.setVisible(true);
256             mShowSystemMenu.setEnabled(false);
257 
258             mHideSystemMenu.setVisible(false);
259             mHideSystemMenu.setEnabled(false);
260         }
261     }
262 
263     @Override
onOptionsItemSelected(MenuItem item)264     public boolean onOptionsItemSelected(MenuItem item) {
265         switch (item.getItemId()) {
266             case android.R.id.home:
267                 getActivity().finishAfterTransition();
268                 return true;
269             case MENU_SHOW_SYSTEM:
270             case MENU_HIDE_SYSTEM:
271                 mShowSystem = item.getItemId() == MENU_SHOW_SYSTEM;
272                 // We already loaded all data, so don't reload
273                 updateUI();
274                 updateMenu();
275                 break;
276         }
277 
278         return super.onOptionsItemSelected(item);
279     }
280 
updateUI()281     private void updateUI() {
282         if (mAppPermissionUsages.isEmpty() || getActivity() == null) {
283             return;
284         }
285         Context context = getActivity();
286         PreferenceScreen screen = getPreferenceScreen();
287         if (screen == null) {
288             screen = getPreferenceManager().createPreferenceScreen(context);
289             setPreferenceScreen(screen);
290         }
291         screen.removeAll();
292 
293         final TimeFilterItem timeFilterItem = mFilterTimes.get(mFilterTimeIndex);
294         long curTime = System.currentTimeMillis();
295         long startTime = Math.max(timeFilterItem == null ? 0 : (curTime - timeFilterItem.getTime()),
296                 0);
297 
298         Set<String> exemptedPackages = Utils.getExemptedPackages(mRoleManager);
299 
300         Preference subtitlePreference = new Preference(context);
301         subtitlePreference.setSummary(
302                 getResources().getString(R.string.permission_group_usage_subtitle,
303                 KotlinUtils.INSTANCE.getPermGroupLabel(getActivity(), mFilterGroup)));
304         subtitlePreference.setSelectable(false);
305         screen.addPreference(subtitlePreference);
306 
307         AtomicBoolean seenSystemApp = new AtomicBoolean(false);
308 
309         ArrayList<PermissionApps.PermissionApp> permApps = new ArrayList<>();
310         List<AppPermissionUsageEntry> usages = mAppPermissionUsages.stream()
311                 .filter(appUsage -> !exemptedPackages.contains(appUsage.getPackageName()))
312                 .map(appUsage -> {
313             // Fetch the access time list of the app accesses mFilterGroup permission group
314             // The DiscreteAccessTime is a Triple of (access time, access duration, proxy) of that
315             // app
316                     List<Triple<Long, Long, OpEventProxyInfo>> discreteAccessTimeList =
317                             new ArrayList<>();
318                     List<AppPermissionUsage.GroupUsage> appGroups = appUsage.getGroupUsages();
319                     int numGroups = appGroups.size();
320                     for (int groupIndex = 0; groupIndex < numGroups; groupIndex++) {
321                         AppPermissionUsage.GroupUsage groupUsage = appGroups.get(groupIndex);
322                         if (!groupUsage.getGroup().getName().equals(mFilterGroup)
323                                 || !groupUsage.hasDiscreteData()) {
324                             continue;
325                         }
326 
327                         final boolean isSystemApp = !Utils.isGroupOrBgGroupUserSensitive(
328                                 groupUsage.getGroup());
329                         seenSystemApp.set(seenSystemApp.get() || isSystemApp);
330                         if (isSystemApp && !mShowSystem) {
331                             continue;
332                         }
333 
334                         List<Triple<Long, Long, OpEventProxyInfo>> allDiscreteAccessTime =
335                                 groupUsage.getAllDiscreteAccessTime();
336                         int numAllDiscreteAccessTime = allDiscreteAccessTime.size();
337                         for (int discreteAccessTimeIndex = 0;
338                                 discreteAccessTimeIndex < numAllDiscreteAccessTime;
339                                 discreteAccessTimeIndex++) {
340                             Triple<Long, Long, OpEventProxyInfo> discreteAccessTime =
341                                     allDiscreteAccessTime.get(discreteAccessTimeIndex);
342                             if (discreteAccessTime.getFirst() == 0
343                                     || discreteAccessTime.getFirst() < startTime) {
344                                 continue;
345                             }
346 
347                             discreteAccessTimeList.add(discreteAccessTime);
348                         }
349                     }
350 
351                     Collections.sort(
352                             discreteAccessTimeList, (x, y) -> y.getFirst().compareTo(x.getFirst()));
353 
354                     if (discreteAccessTimeList.size() > 0) {
355                         permApps.add(appUsage.getApp());
356                     }
357 
358                     // If the current permission group is not LOCATION or there's only one access
359                     // for the app, return individual entry early.
360                     if (!ALLOW_CLUSTERING_PERMISSION_GROUPS.contains(mFilterGroup)
361                             || discreteAccessTimeList.size() <= 1) {
362                         return discreteAccessTimeList.stream().map(
363                                 time -> new AppPermissionUsageEntry(appUsage, time.getFirst(),
364                                         Collections.singletonList(time)))
365                                 .collect(Collectors.toList());
366                     }
367 
368                     // Group access time list
369                     List<AppPermissionUsageEntry> usageEntries = new ArrayList<>();
370                     AppPermissionUsageEntry ongoingEntry = null;
371                     for (Triple<Long, Long, OpEventProxyInfo> time : discreteAccessTimeList) {
372                         if (ongoingEntry == null) {
373                             ongoingEntry = new AppPermissionUsageEntry(appUsage, time.getFirst(),
374                                     Stream.of(time)
375                                             .collect(Collectors.toCollection(ArrayList::new)));
376                         } else {
377                             List<Triple<Long, Long, OpEventProxyInfo>> ongoingAccessTimeList =
378                                     ongoingEntry.mClusteredAccessTimeList;
379                             if (time.getFirst() / ONE_HOUR_MS
380                                     != ongoingAccessTimeList.get(0).getFirst() / ONE_HOUR_MS
381                                     || ongoingAccessTimeList.get(ongoingAccessTimeList.size() - 1)
382                                     .getFirst()
383                                     / ONE_MINUTE_MS - time.getFirst() / ONE_MINUTE_MS
384                                     > CLUSTER_MINUTES_APART) {
385                                 // If the current access time is not in the same hour nor within
386                                 // CLUSTER_MINUTES_APART, add the ongoing entry to the usage list
387                                 // and start a new ongoing entry.
388                                 usageEntries.add(ongoingEntry);
389                                 ongoingEntry = new AppPermissionUsageEntry(appUsage,
390                                         time.getFirst(), Stream.of(time)
391                                         .collect(Collectors.toCollection(ArrayList::new)));
392                             } else {
393                                 ongoingAccessTimeList.add(time);
394                             }
395                         }
396                     }
397                     usageEntries.add(ongoingEntry);
398 
399                     return usageEntries;
400                 }).flatMap(Collection::stream).sorted((x, y) -> {
401                     // Sort all usage entries by startTime desc, and then by app name.
402                     int timeCompare = Long.compare(y.mEndTime, x.mEndTime);
403                     if (timeCompare != 0) {
404                         return timeCompare;
405                     }
406                     return x.mAppPermissionUsage.getApp().getLabel().compareTo(
407                             y.mAppPermissionUsage.getApp().getLabel());
408                 }).collect(Collectors.toList());
409 
410         if (mHasSystemApps != seenSystemApp.get()) {
411             mHasSystemApps = seenSystemApp.get();
412             getActivity().invalidateOptionsMenu();
413         }
414 
415         // Truncate to midnight in current timezone.
416         final long midnightToday = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).toEpochSecond()
417                 * 1000L;
418         AppPermissionUsageEntry midnightTodayEntry = new AppPermissionUsageEntry(
419                 null, midnightToday, null);
420 
421         // Use the placeholder pair midnightTodayPair to get
422         // the index of the first usage entry from yesterday
423         int todayCategoryIndex = 0;
424         int yesterdayCategoryIndex = Collections.binarySearch(usages,
425                 midnightTodayEntry, (e1, e2) -> Long.compare(e2.getEndTime(), e1.getEndTime()));
426         if (yesterdayCategoryIndex < 0) {
427             yesterdayCategoryIndex = -1 * (yesterdayCategoryIndex + 1);
428         }
429 
430         // Make these variables effectively final so that
431         // we can use these captured variables in the below lambda expression
432         AtomicReference<PreferenceCategory> category = new AtomicReference<>(
433                 createDayCategoryPreference(context));
434         screen.addPreference(category.get());
435         PreferenceScreen finalScreen = screen;
436         int finalYesterdayCategoryIndex = yesterdayCategoryIndex;
437 
438         new PermissionApps.AppDataLoader(context, () -> {
439             final int numUsages = usages.size();
440             for (int usageNum = 0; usageNum < numUsages; usageNum++) {
441                 AppPermissionUsageEntry usage = usages.get(usageNum);
442                 if (finalYesterdayCategoryIndex == usageNum) {
443                     if (finalYesterdayCategoryIndex != todayCategoryIndex) {
444                         // We create a new category only when we need it.
445                         // We will not create a new category if we only need one category for
446                         // either today's or yesterday's usage
447                         category.set(createDayCategoryPreference(context));
448                         finalScreen.addPreference(category.get());
449                     }
450                     category.get().setTitle(R.string.permission_history_category_yesterday);
451                 } else if (todayCategoryIndex == usageNum) {
452                     category.get().setTitle(R.string.permission_history_category_today);
453                 }
454 
455                 String accessTime = DateFormat.getTimeFormat(context).format(usage.mEndTime);
456                 Long durationLong = usage.mClusteredAccessTimeList
457                         .stream()
458                         .map(p -> p.getSecond())
459                         .filter(dur -> dur > 0)
460                         .reduce(0L, (dur1, dur2) -> dur1 + dur2);
461 
462                 List<Long> accessTimeList = usage.mClusteredAccessTimeList
463                         .stream().map(p -> p.getFirst()).collect(Collectors.toList());
464                 ArrayList<String> attributionTags =
465                         usage.mAppPermissionUsage.getGroupUsages().stream().filter(groupUsage ->
466                                 groupUsage.getGroup().getName().equals(mFilterGroup)).map(
467                                 AppPermissionUsage.GroupUsage::getAttributionTags).filter(
468                                 Objects::nonNull).flatMap(Collection::stream).collect(
469                                 Collectors.toCollection(ArrayList::new));
470 
471                 // Determine the preference summary. Start with the duration string
472                 String summaryLabel = null;
473                 // Since Location accesses are atomic, we manually calculate the access duration
474                 // by comparing the first and last access within the cluster
475                 if (mFilterGroup.equals(permission_group.LOCATION)) {
476                     if (accessTimeList.size() > 1) {
477                         durationLong = accessTimeList.get(0)
478                                 - accessTimeList.get(accessTimeList.size() - 1);
479 
480                         // Similar to other history items, only show the duration if it's longer
481                         // than the clustering granularity.
482                         if (durationLong
483                                 >= (MINUTES.toMillis(CLUSTER_MINUTES_APART) + 1)) {
484                             summaryLabel = UtilsKt.getDurationUsedStr(context, durationLong);
485                         }
486                     }
487                 } else {
488                     // Only show the duration if it is at least (cluster + 1) minutes. Displaying
489                     // times that are the same as the cluster granularity does not convey useful
490                     // information.
491                     if ((durationLong != null)
492                             && durationLong >= MINUTES.toMillis(CLUSTER_MINUTES_APART + 1)) {
493                         summaryLabel = UtilsKt.getDurationUsedStr(context, durationLong);
494                     }
495                 }
496 
497                 String proxyPackageLabel = null;
498                 for (int i = 0; i < usage.mClusteredAccessTimeList.size(); i++) {
499                     OpEventProxyInfo proxy = usage.mClusteredAccessTimeList.get(i).getThird();
500                     if (proxy != null && proxy.getPackageName() != null) {
501                         proxyPackageLabel = KotlinUtils.INSTANCE.getPackageLabel(
502                                 PermissionControllerApplication.get(), proxy.getPackageName(),
503                                 UserHandle.getUserHandleForUid(proxy.getUid()));
504                         break;
505                     }
506                 }
507 
508                 // if we have both a proxy and a duration, combine the two.
509                 if (summaryLabel != null && proxyPackageLabel != null) {
510                     summaryLabel = context.getString(R.string.permission_usage_duration_and_proxy,
511                             proxyPackageLabel, summaryLabel);
512                 } else {
513                     summaryLabel = proxyPackageLabel;
514                 }
515 
516                 PermissionHistoryPreference permissionUsagePreference = new
517                         PermissionHistoryPreference(context,
518                         UserHandle.getUserHandleForUid(usage.mAppPermissionUsage.getApp().getUid()),
519                         usage.mAppPermissionUsage.getPackageName(),
520                         usage.mAppPermissionUsage.getApp().getIcon(),
521                         usage.mAppPermissionUsage.getApp().getLabel(),
522                         mFilterGroup, accessTime, summaryLabel, accessTimeList, attributionTags,
523                         usageNum == (numUsages - 1),
524                         mSessionId
525                 );
526 
527                 category.get().addPreference(permissionUsagePreference);
528             }
529 
530             setLoading(false, true);
531             mFinishedInitialLoad = true;
532             setProgressBarVisible(false);
533             mPermissionUsages.stopLoader(getActivity().getLoaderManager());
534 
535         }).execute(permApps.toArray(new PermissionApps.PermissionApp[permApps.size()]));
536     }
537 
createDayCategoryPreference(Context context)538     private PreferenceCategory createDayCategoryPreference(Context context) {
539         PreferenceCategory category = new PreferenceCategory(context);
540         // Do not reserve icon space, so that the text moves all the way left.
541         category.setIconSpaceReserved(false);
542         return category;
543     }
544 
545     /**
546      * Get an AppPermissionGroup that represents the given permission group (and an arbitrary app).
547      *
548      * @param groupName The name of the permission group.
549      *
550      * @return an AppPermissionGroup representing the given permission group or null if no such
551      * AppPermissionGroup is found.
552      */
getGroup(@onNull String groupName)553     private @Nullable AppPermissionGroup getGroup(@NonNull String groupName) {
554         List<AppPermissionGroup> groups = getOSPermissionGroups();
555         int numGroups = groups.size();
556         for (int i = 0; i < numGroups; i++) {
557             if (groups.get(i).getName().equals(groupName)) {
558                 return groups.get(i);
559             }
560         }
561         return null;
562     }
563 
564     /**
565      * Get the permission groups declared by the OS.
566      *
567      * TODO: theianchen change the method name to make that clear,
568      * and return a list of string group names, not AppPermissionGroups.
569      * @return a list of the permission groups declared by the OS.
570      */
getOSPermissionGroups()571     private @NonNull List<AppPermissionGroup> getOSPermissionGroups() {
572         final List<AppPermissionGroup> groups = new ArrayList<>();
573         final Set<String> seenGroups = new ArraySet<>();
574         final int numGroups = mAppPermissionUsages.size();
575         for (int i = 0; i < numGroups; i++) {
576             final AppPermissionUsage appUsage = mAppPermissionUsages.get(i);
577             final List<AppPermissionUsage.GroupUsage> groupUsages = appUsage.getGroupUsages();
578             final int groupUsageCount = groupUsages.size();
579             for (int j = 0; j < groupUsageCount; j++) {
580                 final AppPermissionUsage.GroupUsage groupUsage = groupUsages.get(j);
581                 if (Utils.isModernPermissionGroup(groupUsage.getGroup().getName())) {
582                     if (seenGroups.add(groupUsage.getGroup().getName())) {
583                         groups.add(groupUsage.getGroup());
584                     }
585                 }
586             }
587         }
588         return groups;
589     }
590 
reloadData()591     private void reloadData() {
592         final TimeFilterItem timeFilterItem = mFilterTimes.get(mFilterTimeIndex);
593         final long filterTimeBeginMillis = Math.max(System.currentTimeMillis()
594                 - timeFilterItem.getTime(), 0);
595         mPermissionUsages.load(null /*filterPackageName*/, null /*filterPermissionGroups*/,
596                 filterTimeBeginMillis, Long.MAX_VALUE, PermissionUsages.USAGE_FLAG_LAST
597                         | PermissionUsages.USAGE_FLAG_HISTORICAL, getActivity().getLoaderManager(),
598                 false /*getUiInfo*/, false /*getNonPlatformPermissions*/, this /*callback*/,
599                 false /*sync*/);
600         if (mFinishedInitialLoad) {
601             setProgressBarVisible(true);
602         }
603     }
604 
605     /**
606      * Initialize the time filter to show the smallest entry greater than the time passed in as an
607      * argument.  If nothing is passed, this simply initializes the possible values.
608      */
initializeTimeFilter()609     private void initializeTimeFilter() {
610         Context context = getPreferenceManager().getContext();
611         mFilterTimes = new ArrayList<>();
612         mFilterTimes.add(new TimeFilterItem(Long.MAX_VALUE,
613                 context.getString(R.string.permission_usage_any_time)));
614         mFilterTimes.add(new TimeFilterItem(DAYS.toMillis(7),
615                 context.getString(R.string.permission_usage_last_7_days)));
616         mFilterTimes.add(new TimeFilterItem(DAYS.toMillis(1),
617                 context.getString(R.string.permission_usage_last_day)));
618         mFilterTimes.add(new TimeFilterItem(HOURS.toMillis(1),
619                 context.getString(R.string.permission_usage_last_hour)));
620         mFilterTimes.add(new TimeFilterItem(MINUTES.toMillis(15),
621                 context.getString(R.string.permission_usage_last_15_minutes)));
622         mFilterTimes.add(new TimeFilterItem(MINUTES.toMillis(1),
623                 context.getString(R.string.permission_usage_last_minute)));
624 
625         // TODO: theianchen add code for filtering by time here.
626     }
627 
628     /**
629      * A class representing a given time, e.g., "in the last hour".
630      */
631     private static class TimeFilterItem {
632         private final long mTime;
633         private final @NonNull String mLabel;
634 
TimeFilterItem(long time, @NonNull String label)635         TimeFilterItem(long time, @NonNull String label) {
636             mTime = time;
637             mLabel = label;
638         }
639 
640         /**
641          * Get the time represented by this object in milliseconds.
642          *
643          * @return the time represented by this object.
644          */
getTime()645         public long getTime() {
646             return mTime;
647         }
648 
getLabel()649         public @NonNull String getLabel() {
650             return mLabel;
651         }
652     }
653 
654     /**
655      * A class representing an app usage entry in Permission Usage.
656      */
657     private static class AppPermissionUsageEntry {
658         private final AppPermissionUsage mAppPermissionUsage;
659         private final List<Triple<Long, Long, OpEventProxyInfo>> mClusteredAccessTimeList;
660         private long mEndTime;
661 
AppPermissionUsageEntry(AppPermissionUsage appPermissionUsage, long endTime, List<Triple<Long, Long, OpEventProxyInfo>> clusteredAccessTimeList)662         AppPermissionUsageEntry(AppPermissionUsage appPermissionUsage, long endTime,
663                 List<Triple<Long, Long, OpEventProxyInfo>> clusteredAccessTimeList) {
664             mAppPermissionUsage = appPermissionUsage;
665             mEndTime = endTime;
666             mClusteredAccessTimeList = clusteredAccessTimeList;
667         }
668 
getAppPermissionUsage()669         public AppPermissionUsage getAppPermissionUsage() {
670             return mAppPermissionUsage;
671         }
672 
getEndTime()673         public long getEndTime() {
674             return mEndTime;
675         }
676 
getAccessTime()677         public List<Triple<Long, Long, OpEventProxyInfo>> getAccessTime() {
678             return mClusteredAccessTimeList;
679         }
680     }
681 }
682