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