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