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