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 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_USAGE_FRAGMENT_INTERACTION;
22 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SEE_OTHER_PERMISSIONS_CLICKED;
23 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED;
24 import static com.android.permissioncontroller.PermissionControllerStatsLog.write;
25 
26 import static java.util.concurrent.TimeUnit.DAYS;
27 
28 import android.Manifest;
29 import android.app.ActionBar;
30 import android.app.Activity;
31 import android.app.role.RoleManager;
32 import android.content.Context;
33 import android.os.Build;
34 import android.os.Bundle;
35 import android.util.ArrayMap;
36 import android.util.ArraySet;
37 import android.util.Log;
38 import android.view.Menu;
39 import android.view.MenuInflater;
40 import android.view.MenuItem;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.annotation.RequiresApi;
45 import androidx.preference.Preference;
46 import androidx.preference.PreferenceCategory;
47 import androidx.preference.PreferenceGroupAdapter;
48 import androidx.preference.PreferenceScreen;
49 import androidx.recyclerview.widget.RecyclerView;
50 
51 import com.android.permissioncontroller.R;
52 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
53 import com.android.permissioncontroller.permission.model.AppPermissionUsage;
54 import com.android.permissioncontroller.permission.model.AppPermissionUsage.GroupUsage;
55 import com.android.permissioncontroller.permission.model.PermissionUsages;
56 import com.android.permissioncontroller.permission.model.legacy.PermissionApps;
57 import com.android.permissioncontroller.permission.ui.handheld.PermissionUsageV2ControlPreference;
58 import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader;
59 import com.android.permissioncontroller.permission.utils.KotlinUtils;
60 import com.android.permissioncontroller.permission.utils.Utils;
61 import com.android.settingslib.HelpUtils;
62 
63 import java.time.Instant;
64 import java.util.ArrayList;
65 import java.util.HashMap;
66 import java.util.List;
67 import java.util.Map;
68 import java.util.Set;
69 
70 /**
71  * The main page for the privacy dashboard.
72  */
73 @RequiresApi(Build.VERSION_CODES.S)
74 public class PermissionUsageV2Fragment extends SettingsWithLargeHeader implements
75         PermissionUsages.PermissionsUsagesChangeCallback {
76     private static final String LOG_TAG = "PermUsageV2Fragment";
77 
78     private static final int MENU_REFRESH = MENU_HIDE_SYSTEM + 1;
79 
80     /** TODO(ewol): Use the config setting to determine amount of time to show. */
81     private static final long TIME_FILTER_MILLIS = DAYS.toMillis(1);
82 
83     private static final Map<String, Integer> PERMISSION_GROUP_ORDER = Map.of(
84             Manifest.permission_group.LOCATION, 0,
85             Manifest.permission_group.CAMERA, 1,
86             Manifest.permission_group.MICROPHONE, 2
87     );
88     private static final int DEFAULT_ORDER = 3;
89 
90     // Pie chart in this screen will be the first child.
91     // Hence we use PERMISSION_GROUP_ORDER + 1 here.
92     private static final int PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT =
93             PERMISSION_GROUP_ORDER.size() + 1;
94     private static final int EXPAND_BUTTON_ORDER = 999;
95 
96     private static final String KEY_SESSION_ID = "_session_id";
97     private static final String SESSION_ID_KEY = PermissionUsageV2Fragment.class.getName()
98             + KEY_SESSION_ID;
99 
100     private @NonNull PermissionUsages mPermissionUsages;
101     private @Nullable List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>();
102 
103     private boolean mShowSystem;
104     private boolean mHasSystemApps;
105     private MenuItem mShowSystemMenu;
106     private MenuItem mHideSystemMenu;
107     private boolean mOtherExpanded;
108 
109     private ArrayMap<String, Integer> mGroupAppCounts = new ArrayMap<>();
110 
111     private boolean mFinishedInitialLoad;
112 
113     private @NonNull RoleManager mRoleManager;
114 
115     private PermissionUsageGraphicPreference mGraphic;
116 
117     /** Unique Id of a request */
118     private long mSessionId;
119 
120     @Override
onCreate(Bundle savedInstanceState)121     public void onCreate(Bundle savedInstanceState) {
122         super.onCreate(savedInstanceState);
123 
124         if (savedInstanceState != null) {
125             mSessionId = savedInstanceState.getLong(SESSION_ID_KEY);
126         } else {
127             mSessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
128         }
129 
130         mFinishedInitialLoad = false;
131 
132         // By default, do not show system app usages.
133         mShowSystem = false;
134 
135         // Start out with 'other' permissions not expanded.
136         mOtherExpanded = false;
137 
138         setLoading(true, false);
139         setHasOptionsMenu(true);
140         ActionBar ab = getActivity().getActionBar();
141         if (ab != null) {
142             ab.setDisplayHomeAsUpEnabled(true);
143         }
144 
145         Context context = getPreferenceManager().getContext();
146         mPermissionUsages = new PermissionUsages(context);
147         mRoleManager = Utils.getSystemServiceSafe(context, RoleManager.class);
148 
149         reloadData();
150     }
151 
152     @Override
onCreateAdapter(PreferenceScreen preferenceScreen)153     public RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
154         PreferenceGroupAdapter adapter =
155                 (PreferenceGroupAdapter) super.onCreateAdapter(preferenceScreen);
156 
157         adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
158             @Override
159             public void onChanged() {
160                 updatePreferenceScreenAdvancedTitleAndSummary(preferenceScreen, adapter);
161             }
162 
163             @Override
164             public void onItemRangeInserted(int positionStart, int itemCount) {
165                 onChanged();
166             }
167 
168             @Override
169             public void onItemRangeRemoved(int positionStart, int itemCount) {
170                 onChanged();
171             }
172 
173             @Override
174             public void onItemRangeChanged(int positionStart, int itemCount) {
175                 onChanged();
176             }
177 
178             @Override
179             public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
180                 onChanged();
181             }
182         });
183 
184         updatePreferenceScreenAdvancedTitleAndSummary(preferenceScreen, adapter);
185         return adapter;
186     }
187 
updatePreferenceScreenAdvancedTitleAndSummary(PreferenceScreen preferenceScreen, PreferenceGroupAdapter adapter)188     private void updatePreferenceScreenAdvancedTitleAndSummary(PreferenceScreen preferenceScreen,
189             PreferenceGroupAdapter adapter) {
190         int count = adapter.getItemCount();
191         if (count == 0) {
192             return;
193         }
194 
195         Preference preference = adapter.getItem(count - 1);
196 
197         // This is a hacky way of getting the expand button preference for advanced info
198         if (preference.getOrder() == EXPAND_BUTTON_ORDER) {
199             mOtherExpanded = false;
200             preference.setTitle(R.string.perm_usage_adv_info_title);
201             preference.setSummary(preferenceScreen.getSummary());
202             preference.setLayoutResource(R.layout.expand_button_with_large_title);
203             if (mGraphic != null) {
204                 mGraphic.setShowOtherCategory(false);
205             }
206         } else {
207             mOtherExpanded = true;
208             if (mGraphic != null) {
209                 mGraphic.setShowOtherCategory(true);
210             }
211         }
212     }
213 
214     @Override
onStart()215     public void onStart() {
216         super.onStart();
217         getActivity().setTitle(R.string.permission_usage_title);
218 
219     }
220 
221     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)222     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
223         super.onCreateOptionsMenu(menu, inflater);
224         if (mHasSystemApps) {
225             mShowSystemMenu = menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE,
226                     R.string.menu_show_system);
227             mHideSystemMenu = menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE,
228                     R.string.menu_hide_system);
229         }
230 
231         HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_permission_usage,
232                 getClass().getName());
233         MenuItem refresh = menu.add(Menu.NONE, MENU_REFRESH, Menu.NONE,
234                 R.string.permission_usage_refresh);
235         refresh.setIcon(R.drawable.ic_refresh);
236         refresh.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
237         updateMenu();
238     }
239 
240     @Override
onOptionsItemSelected(MenuItem item)241     public boolean onOptionsItemSelected(MenuItem item) {
242         switch (item.getItemId()) {
243             case android.R.id.home:
244                 getActivity().finishAfterTransition();
245                 return true;
246             case MENU_SHOW_SYSTEM:
247                 write(PERMISSION_USAGE_FRAGMENT_INTERACTION, mSessionId,
248                         PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED);
249                 // fall through
250             case MENU_HIDE_SYSTEM:
251                 mShowSystem = item.getItemId() == MENU_SHOW_SYSTEM;
252                 // We already loaded all data, so don't reload
253                 updateUI();
254                 updateMenu();
255                 break;
256             case MENU_REFRESH:
257                 reloadData();
258                 break;
259         }
260         return super.onOptionsItemSelected(item);
261     }
262 
updateMenu()263     private void updateMenu() {
264         if (mHasSystemApps) {
265             mShowSystemMenu.setVisible(!mShowSystem);
266             mHideSystemMenu.setVisible(mShowSystem);
267         }
268     }
269 
270     @Override
onPermissionUsagesChanged()271     public void onPermissionUsagesChanged() {
272         if (mPermissionUsages.getUsages().isEmpty()) {
273             return;
274         }
275         mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages());
276         updateUI();
277     }
278 
279     @Override
getEmptyViewString()280     public int getEmptyViewString() {
281         return R.string.no_permission_usages;
282     }
283 
284     @Override
onSaveInstanceState(Bundle outState)285     public void onSaveInstanceState(Bundle outState) {
286         super.onSaveInstanceState(outState);
287         if (outState != null) {
288             outState.putLong(SESSION_ID_KEY, mSessionId);
289         }
290     }
291 
updateUI()292     private void updateUI() {
293         if (mAppPermissionUsages.isEmpty() || getActivity() == null) {
294             return;
295         }
296         Context context = getActivity();
297 
298         PreferenceScreen screen = getPreferenceScreen();
299         if (screen == null) {
300             screen = getPreferenceManager().createPreferenceScreen(context);
301             setPreferenceScreen(screen);
302         }
303         screen.removeAll();
304 
305         if (mOtherExpanded) {
306             screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
307         } else {
308             screen.setInitialExpandedChildrenCount(
309                     PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT);
310         }
311         screen.setOnExpandButtonClickListener(() -> {
312             write(PERMISSION_USAGE_FRAGMENT_INTERACTION, mSessionId,
313                     PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SEE_OTHER_PERMISSIONS_CLICKED);
314         });
315 
316         long curTime = System.currentTimeMillis();
317         long startTime = Math.max(curTime - TIME_FILTER_MILLIS,
318                 Instant.EPOCH.toEpochMilli());
319 
320         mGroupAppCounts.clear();
321         // Permission group to count mapping.
322         Map<String, Integer> usages = new HashMap<>();
323         List<AppPermissionGroup> permissionGroups = getOSPermissionGroups();
324         for (int i = 0; i < permissionGroups.size(); i++) {
325             usages.put(permissionGroups.get(i).getName(), 0);
326         }
327         ArrayList<PermissionApps.PermissionApp> permApps = new ArrayList<>();
328 
329         Set<String> exemptedPackages = Utils.getExemptedPackages(mRoleManager);
330 
331         boolean seenSystemApp = extractPermissionUsage(exemptedPackages,
332                 usages, permApps, startTime);
333 
334         if (mHasSystemApps != seenSystemApp) {
335             mHasSystemApps = seenSystemApp;
336             getActivity().invalidateOptionsMenu();
337         }
338 
339         mGraphic = new PermissionUsageGraphicPreference(context);
340         screen.addPreference(mGraphic);
341         mGraphic.setUsages(usages);
342 
343         // Add the preference header.
344         PreferenceCategory category = new PreferenceCategory(context);
345         screen.addPreference(category);
346 
347         Map<String, CharSequence> groupUsageNameToLabel = new HashMap<>();
348         List<Map.Entry<String, Integer>> groupUsagesList = new ArrayList<>(usages.entrySet());
349         int usagesEntryCount = groupUsagesList.size();
350         for (int usageEntryIndex = 0; usageEntryIndex < usagesEntryCount; usageEntryIndex++) {
351             Map.Entry<String, Integer> usageEntry = groupUsagesList.get(usageEntryIndex);
352             groupUsageNameToLabel.put(usageEntry.getKey(),
353                     KotlinUtils.INSTANCE.getPermGroupLabel(context, usageEntry.getKey()));
354         }
355 
356         groupUsagesList.sort((e1, e2) -> comparePermissionGroupUsage(
357                 e1, e2, groupUsageNameToLabel));
358 
359         CharSequence advancedInfoSummary = getAdvancedInfoSummaryString(context, groupUsagesList);
360         screen.setSummary(advancedInfoSummary);
361 
362         addUIContent(context, groupUsagesList, permApps, category);
363     }
364 
getAdvancedInfoSummaryString(Context context, List<Map.Entry<String, Integer>> groupUsagesList)365     private CharSequence getAdvancedInfoSummaryString(Context context,
366             List<Map.Entry<String, Integer>> groupUsagesList) {
367         int size = groupUsagesList.size();
368         if (size <= PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1) {
369             return "";
370         }
371 
372         // case for 1 extra item in the advanced info
373         if (size == PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT) {
374             String permGroupName = groupUsagesList
375                     .get(PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1).getKey();
376             return KotlinUtils.INSTANCE.getPermGroupLabel(context, permGroupName);
377         }
378 
379         String permGroupName1 = groupUsagesList
380                 .get(PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1).getKey();
381         String permGroupName2 = groupUsagesList
382                 .get(PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT).getKey();
383         CharSequence permGroupLabel1 = KotlinUtils
384                 .INSTANCE.getPermGroupLabel(context, permGroupName1);
385         CharSequence permGroupLabel2 = KotlinUtils
386                 .INSTANCE.getPermGroupLabel(context, permGroupName2);
387 
388         // case for 2 extra items in the advanced info
389         if (size == PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT + 1) {
390             return context.getResources().getString(R.string.perm_usage_adv_info_summary_2_items,
391                     permGroupLabel1, permGroupLabel2);
392         }
393 
394         // case for 3 or more extra items in the advanced info
395         int numExtraItems = size - PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1;
396         return context.getResources().getString(R.string.perm_usage_adv_info_summary_more_items,
397                 permGroupLabel1, permGroupLabel2, numExtraItems);
398     }
399 
400     /**
401      * Extract the permission usages from mAppPermissionUsages and put the extracted usages
402      * into usages and permApps. Returns whether we have seen a system app during the process.
403      *
404      * TODO: theianchen
405      * It's doing two things at the same method which is violating the SOLID principle.
406      * We should fix this.
407      *
408      * @param exemptedPackages packages that are the role holders for exempted roles
409      * @param usages an empty List that will be filled with permission usages.
410      * @param permApps an empty List that will be filled with permission apps.
411      * @return whether we have seen a system app.
412      */
extractPermissionUsage(Set<String> exemptedPackages, Map<String, Integer> usages, ArrayList<PermissionApps.PermissionApp> permApps, long startTime)413     private boolean extractPermissionUsage(Set<String> exemptedPackages,
414             Map<String, Integer> usages,
415             ArrayList<PermissionApps.PermissionApp> permApps,
416             long startTime) {
417         boolean seenSystemApp = false;
418         int numApps = mAppPermissionUsages.size();
419         for (int appNum = 0; appNum < numApps; appNum++) {
420             AppPermissionUsage appUsage = mAppPermissionUsages.get(appNum);
421             if (exemptedPackages.contains(appUsage.getPackageName())) {
422                 continue;
423             }
424 
425             boolean used = false;
426             List<GroupUsage> appGroups = appUsage.getGroupUsages();
427             int numGroups = appGroups.size();
428             for (int groupNum = 0; groupNum < numGroups; groupNum++) {
429                 GroupUsage groupUsage = appGroups.get(groupNum);
430                 String groupName = groupUsage.getGroup().getName();
431                 long lastAccessTime = groupUsage.getLastAccessTime();
432                 if (lastAccessTime == 0) {
433                     Log.w(LOG_TAG,
434                             "Unexpected access time of 0 for " + appUsage.getApp().getKey() + " "
435                                     + groupUsage.getGroup().getName());
436                     continue;
437                 }
438                 if (lastAccessTime < startTime) {
439                     continue;
440                 }
441 
442                 final boolean isSystemApp = !Utils.isGroupOrBgGroupUserSensitive(
443                         groupUsage.getGroup());
444                 seenSystemApp = seenSystemApp || isSystemApp;
445 
446                 // If not showing system apps, skip.
447                 if (!mShowSystem && isSystemApp) {
448                     continue;
449                 }
450 
451                 used = true;
452                 addGroupUser(groupName);
453 
454                 usages.put(groupName, usages.getOrDefault(groupName, 0) + 1);
455             }
456             if (used) {
457                 permApps.add(appUsage.getApp());
458                 addGroupUser(null);
459             }
460         }
461 
462         return seenSystemApp;
463     }
464 
465     /**
466      * Use the usages and permApps that are previously constructed to add UI content to the page
467      */
addUIContent(Context context, List<Map.Entry<String, Integer>> usages, ArrayList<PermissionApps.PermissionApp> permApps, PreferenceCategory category)468     private void addUIContent(Context context,
469             List<Map.Entry<String, Integer>> usages,
470             ArrayList<PermissionApps.PermissionApp> permApps,
471             PreferenceCategory category) {
472         new PermissionApps.AppDataLoader(context, () -> {
473             for (int i = 0; i < usages.size(); i++) {
474                 Map.Entry<String, Integer> currentEntry = usages.get(i);
475                 PermissionUsageV2ControlPreference permissionUsagePreference =
476                         new PermissionUsageV2ControlPreference(context, currentEntry.getKey(),
477                                 currentEntry.getValue(), mShowSystem, mSessionId);
478                 category.addPreference(permissionUsagePreference);
479             }
480 
481             setLoading(false, true);
482             mFinishedInitialLoad = true;
483             setProgressBarVisible(false);
484 
485             Activity activity = getActivity();
486             if (activity != null) {
487                 mPermissionUsages.stopLoader(activity.getLoaderManager());
488             }
489         }).execute(permApps.toArray(new PermissionApps.PermissionApp[0]));
490     }
491 
addGroupUser(String app)492     private void addGroupUser(String app) {
493         Integer count = mGroupAppCounts.get(app);
494         if (count == null) {
495             mGroupAppCounts.put(app, 1);
496         } else {
497             mGroupAppCounts.put(app, count + 1);
498         }
499     }
500 
501     /**
502      * Reloads the data to show.
503      */
reloadData()504     private void reloadData() {
505         final long filterTimeBeginMillis = Math.max(System.currentTimeMillis()
506                 - TIME_FILTER_MILLIS, Instant.EPOCH.toEpochMilli());
507         mPermissionUsages.load(null /*filterPackageName*/, null /*filterPermissionGroups*/,
508                 filterTimeBeginMillis, Long.MAX_VALUE, PermissionUsages.USAGE_FLAG_LAST
509                         | PermissionUsages.USAGE_FLAG_HISTORICAL, getActivity().getLoaderManager(),
510                 false /*getUiInfo*/, false /*getNonPlatformPermissions*/, this /*callback*/,
511                 false /*sync*/);
512         if (mFinishedInitialLoad) {
513             setProgressBarVisible(true);
514         }
515     }
516 
comparePermissionGroupUsage(@onNull Map.Entry<String, Integer> first, @NonNull Map.Entry<String, Integer> second, Map<String, CharSequence> groupUsageNameToLabelMapping)517     private static int comparePermissionGroupUsage(@NonNull Map.Entry<String, Integer> first,
518             @NonNull Map.Entry<String, Integer> second,
519             Map<String, CharSequence> groupUsageNameToLabelMapping) {
520         int firstPermissionOrder = PERMISSION_GROUP_ORDER
521                 .getOrDefault(first.getKey(), DEFAULT_ORDER);
522         int secondPermissionOrder = PERMISSION_GROUP_ORDER
523                 .getOrDefault(second.getKey(), DEFAULT_ORDER);
524         if (firstPermissionOrder != secondPermissionOrder) {
525             return firstPermissionOrder - secondPermissionOrder;
526         }
527 
528         return groupUsageNameToLabelMapping.get(first.getKey()).toString()
529                 .compareTo(groupUsageNameToLabelMapping.get(second.getKey()).toString());
530     }
531 
532     /**
533      * Get the permission groups declared by the OS.
534      *
535      * @return a list of the permission groups declared by the OS.
536      */
getOSPermissionGroups()537     private @NonNull List<AppPermissionGroup> getOSPermissionGroups() {
538         final List<AppPermissionGroup> groups = new ArrayList<>();
539         final Set<String> seenGroups = new ArraySet<>();
540         final int numGroups = mAppPermissionUsages.size();
541         for (int i = 0; i < numGroups; i++) {
542             final AppPermissionUsage appUsage = mAppPermissionUsages.get(i);
543             final List<GroupUsage> groupUsages = appUsage.getGroupUsages();
544             final int groupUsageCount = groupUsages.size();
545             for (int j = 0; j < groupUsageCount; j++) {
546                 final GroupUsage groupUsage = groupUsages.get(j);
547                 if (Utils.isModernPermissionGroup(groupUsage.getGroup().getName())) {
548                     if (seenGroups.add(groupUsage.getGroup().getName())) {
549                         groups.add(groupUsage.getGroup());
550                     }
551                 }
552             }
553         }
554         return groups;
555     }
556 }
557