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.v31; 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 android.Manifest; 27 import android.app.ActionBar; 28 import android.content.Context; 29 import android.os.Build; 30 import android.os.Bundle; 31 import android.util.Log; 32 import android.view.Menu; 33 import android.view.MenuInflater; 34 import android.view.MenuItem; 35 36 import androidx.annotation.RequiresApi; 37 import androidx.lifecycle.ViewModelProvider; 38 import androidx.preference.Preference; 39 import androidx.preference.PreferenceCategory; 40 import androidx.preference.PreferenceGroupAdapter; 41 import androidx.preference.PreferenceScreen; 42 import androidx.recyclerview.widget.RecyclerView; 43 44 import com.android.permissioncontroller.R; 45 import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader; 46 import com.android.permissioncontroller.permission.ui.viewmodel.v31.PermissionUsageViewModel; 47 import com.android.permissioncontroller.permission.ui.viewmodel.v31.PermissionUsageViewModelFactory; 48 import com.android.permissioncontroller.permission.ui.viewmodel.v31.PermissionUsagesUiState; 49 import com.android.permissioncontroller.permission.utils.KotlinUtils; 50 import com.android.settingslib.HelpUtils; 51 52 import java.util.ArrayList; 53 import java.util.Comparator; 54 import java.util.List; 55 import java.util.Map; 56 57 /** The main page for the privacy dashboard. */ 58 @RequiresApi(Build.VERSION_CODES.S) 59 public class PermissionUsageFragment extends SettingsWithLargeHeader { 60 private static final String LOG_TAG = PermissionUsageFragment.class.getSimpleName(); 61 private static final Map<String, Integer> PERMISSION_GROUP_ORDER = 62 Map.of( 63 Manifest.permission_group.LOCATION, 0, 64 Manifest.permission_group.CAMERA, 1, 65 Manifest.permission_group.MICROPHONE, 2); 66 private static final int DEFAULT_ORDER = 3; 67 68 public static final boolean DEBUG = true; 69 70 // Pie chart in this screen will be the first child. 71 // Hence we use PERMISSION_GROUP_ORDER + 1 here. 72 private static final int PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT = 73 PERMISSION_GROUP_ORDER.size() + 1; 74 private static final int EXPAND_BUTTON_ORDER = 999; 75 /** Map to represent ordering for permission groups in the permissions usage UI. */ 76 private static final String KEY_SESSION_ID = "_session_id"; 77 78 private static final String SESSION_ID_KEY = 79 PermissionUsageFragment.class.getName() + KEY_SESSION_ID; 80 81 private static final int MENU_SHOW_7_DAYS_DATA = Menu.FIRST + 4; 82 private static final int MENU_SHOW_24_HOURS_DATA = Menu.FIRST + 5; 83 84 private PermissionUsageViewModel mViewModel; 85 86 private boolean mHasSystemApps; 87 private MenuItem mShowSystemMenu; 88 private MenuItem mHideSystemMenu; 89 private MenuItem mShow7DaysDataMenu; 90 private MenuItem mShow24HoursDataMenu; 91 private boolean mOtherExpanded; 92 93 private PermissionUsageGraphicPreference mGraphic; 94 95 /** Unique Id of a request */ 96 private long mSessionId; 97 98 @Override onCreate(Bundle savedInstanceState)99 public void onCreate(Bundle savedInstanceState) { 100 super.onCreate(savedInstanceState); 101 if (savedInstanceState != null) { 102 mSessionId = savedInstanceState.getLong(SESSION_ID_KEY); 103 } else { 104 mSessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); 105 } 106 107 PermissionUsageViewModelFactory factory = new PermissionUsageViewModelFactory( 108 getActivity().getApplication(), this, new Bundle()); 109 mViewModel = new ViewModelProvider(this, factory) 110 .get(PermissionUsageViewModel.class); 111 112 // Start out with 'other' permissions not expanded. 113 mOtherExpanded = false; 114 115 setHasOptionsMenu(true); 116 ActionBar ab = getActivity().getActionBar(); 117 if (ab != null) { 118 ab.setDisplayHomeAsUpEnabled(true); 119 } 120 setLoading(true, false); 121 122 mViewModel.getPermissionUsagesUiLiveData().observe(this, this::updateAllUI); 123 } 124 125 @Override onCreateAdapter(PreferenceScreen preferenceScreen)126 public RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { 127 PreferenceGroupAdapter adapter = 128 (PreferenceGroupAdapter) super.onCreateAdapter(preferenceScreen); 129 130 adapter.registerAdapterDataObserver( 131 new RecyclerView.AdapterDataObserver() { 132 @Override 133 public void onChanged() { 134 updatePreferenceScreenAdvancedTitleAndSummary(preferenceScreen, adapter); 135 } 136 137 @Override 138 public void onItemRangeInserted(int positionStart, int itemCount) { 139 onChanged(); 140 } 141 142 @Override 143 public void onItemRangeRemoved(int positionStart, int itemCount) { 144 onChanged(); 145 } 146 147 @Override 148 public void onItemRangeChanged(int positionStart, int itemCount) { 149 onChanged(); 150 } 151 152 @Override 153 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { 154 onChanged(); 155 } 156 }); 157 158 updatePreferenceScreenAdvancedTitleAndSummary(preferenceScreen, adapter); 159 return adapter; 160 } 161 updatePreferenceScreenAdvancedTitleAndSummary( PreferenceScreen preferenceScreen, PreferenceGroupAdapter adapter)162 private void updatePreferenceScreenAdvancedTitleAndSummary( 163 PreferenceScreen preferenceScreen, PreferenceGroupAdapter adapter) { 164 int count = adapter.getItemCount(); 165 if (count == 0) { 166 return; 167 } 168 169 Preference preference = adapter.getItem(count - 1); 170 171 // This is a hacky way of getting the expand button preference for advanced info 172 if (preference.getOrder() == EXPAND_BUTTON_ORDER) { 173 mOtherExpanded = false; 174 preference.setTitle(R.string.perm_usage_adv_info_title); 175 preference.setSummary(preferenceScreen.getSummary()); 176 preference.setLayoutResource(R.layout.expand_button_with_large_title); 177 if (mGraphic != null) { 178 mGraphic.setShowOtherCategory(false); 179 } 180 } else { 181 mOtherExpanded = true; 182 if (mGraphic != null) { 183 mGraphic.setShowOtherCategory(true); 184 } 185 } 186 } 187 188 @Override onStart()189 public void onStart() { 190 super.onStart(); 191 getActivity().setTitle(R.string.permission_usage_title); 192 } 193 194 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)195 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 196 super.onCreateOptionsMenu(menu, inflater); 197 mShowSystemMenu = 198 menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE, R.string.menu_show_system); 199 mHideSystemMenu = 200 menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE, R.string.menu_hide_system); 201 updateShowSystemToggle(mViewModel.getShowSystemApps()); 202 203 if (KotlinUtils.INSTANCE.is7DayToggleEnabled()) { 204 mShow7DaysDataMenu = 205 menu.add( 206 Menu.NONE, 207 MENU_SHOW_7_DAYS_DATA, 208 Menu.NONE, 209 R.string.menu_show_7_days_data); 210 mShow24HoursDataMenu = 211 menu.add( 212 Menu.NONE, 213 MENU_SHOW_24_HOURS_DATA, 214 Menu.NONE, 215 R.string.menu_show_24_hours_data); 216 updateShow7DaysToggle(mViewModel.getShow7DaysData()); 217 } 218 219 HelpUtils.prepareHelpMenuItem( 220 getActivity(), menu, R.string.help_permission_usage, getClass().getName()); 221 } 222 223 @Override onOptionsItemSelected(MenuItem item)224 public boolean onOptionsItemSelected(MenuItem item) { 225 int itemId = item.getItemId(); 226 switch (itemId) { 227 case android.R.id.home: 228 getActivity().finishAfterTransition(); 229 return true; 230 case MENU_SHOW_SYSTEM: 231 write( 232 PERMISSION_USAGE_FRAGMENT_INTERACTION, 233 mSessionId, 234 PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED); 235 updateAllUI(mViewModel.updateShowSystem(true)); 236 break; 237 case MENU_HIDE_SYSTEM: 238 updateAllUI(mViewModel.updateShowSystem(false)); 239 break; 240 case MENU_SHOW_7_DAYS_DATA: 241 updateAllUI(mViewModel.updateShow7Days(KotlinUtils.INSTANCE.is7DayToggleEnabled())); 242 break; 243 case MENU_SHOW_24_HOURS_DATA: 244 updateAllUI(mViewModel.updateShow7Days(false)); 245 break; 246 } 247 return super.onOptionsItemSelected(item); 248 } 249 250 @Override getEmptyViewString()251 public int getEmptyViewString() { 252 return R.string.no_permission_usages; 253 } 254 255 @Override onSaveInstanceState(Bundle outState)256 public void onSaveInstanceState(Bundle outState) { 257 super.onSaveInstanceState(outState); 258 if (outState != null) { 259 outState.putLong(SESSION_ID_KEY, mSessionId); 260 } 261 } 262 updateShowSystemToggle(boolean showSystem)263 private void updateShowSystemToggle(boolean showSystem) { 264 if (mHasSystemApps) { 265 if (mShowSystemMenu != null) { 266 mShowSystemMenu.setVisible(!showSystem); 267 } 268 if (mHideSystemMenu != null) { 269 mHideSystemMenu.setVisible(showSystem); 270 } 271 } else { 272 if (mShowSystemMenu != null) { 273 mShowSystemMenu.setVisible(false); 274 } 275 if (mHideSystemMenu != null) { 276 mHideSystemMenu.setVisible(false); 277 } 278 } 279 } 280 updateShow7DaysToggle(boolean show7Days)281 private void updateShow7DaysToggle(boolean show7Days) { 282 if (mShow7DaysDataMenu != null) { 283 mShow7DaysDataMenu.setVisible(!show7Days); 284 } 285 286 if (mShow24HoursDataMenu != null) { 287 mShow24HoursDataMenu.setVisible(show7Days); 288 } 289 } 290 291 /** Updates page content and menu items. */ updateAllUI(PermissionUsagesUiState uiData)292 private void updateAllUI(PermissionUsagesUiState uiData) { 293 Log.v(LOG_TAG, "Privacy dashboard data = " + uiData); 294 if (getActivity() == null || uiData instanceof PermissionUsagesUiState.Loading) { 295 return; 296 } 297 298 PermissionUsagesUiState.Success permissionUsagesUiData = 299 (PermissionUsagesUiState.Success) uiData; 300 Context context = getActivity(); 301 302 PreferenceScreen screen = getPreferenceScreen(); 303 if (screen == null) { 304 screen = getPreferenceManager().createPreferenceScreen(context); 305 setPreferenceScreen(screen); 306 } 307 screen.removeAll(); 308 309 if (mOtherExpanded) { 310 screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE); 311 } else { 312 screen.setInitialExpandedChildrenCount( 313 PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT); 314 } 315 screen.setOnExpandButtonClickListener(() -> { 316 write( 317 PERMISSION_USAGE_FRAGMENT_INTERACTION, 318 mSessionId, 319 PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SEE_OTHER_PERMISSIONS_CLICKED); 320 }); 321 boolean containsSystemAppUsages = permissionUsagesUiData.getShouldShowSystemToggle(); 322 Map<String, Integer> permissionGroupWithUsageCounts = 323 permissionUsagesUiData.getPermissionGroupUsageCount(); 324 List<Map.Entry<String, Integer>> permissionGroupWithUsageCountsEntries = 325 new ArrayList(permissionGroupWithUsageCounts.entrySet()); 326 327 permissionGroupWithUsageCountsEntries.sort(Comparator.comparing( 328 (Map.Entry<String, Integer> permissionGroupWithUsageCount) -> 329 PERMISSION_GROUP_ORDER.getOrDefault( 330 permissionGroupWithUsageCount.getKey(), DEFAULT_ORDER)) 331 .thenComparing((Map.Entry<String, Integer> permissionGroupWithUsageCount) -> 332 mViewModel.getPermissionGroupLabel( 333 context, permissionGroupWithUsageCount.getKey()))); 334 335 if (mHasSystemApps != containsSystemAppUsages) { 336 mHasSystemApps = containsSystemAppUsages; 337 } 338 339 boolean show7Days = mViewModel.getShow7DaysData(); 340 updateShow7DaysToggle(show7Days); 341 updateShowSystemToggle(mViewModel.getShowSystemApps()); 342 343 mGraphic = new PermissionUsageGraphicPreference(context, show7Days); 344 screen.addPreference(mGraphic); 345 mGraphic.setUsages(permissionGroupWithUsageCounts); 346 347 // Add the preference header. 348 PreferenceCategory category = new PreferenceCategory(context); 349 screen.addPreference(category); 350 CharSequence advancedInfoSummary = 351 getAdvancedInfoSummaryString(context, permissionGroupWithUsageCountsEntries); 352 screen.setSummary(advancedInfoSummary); 353 354 addUIContent(context, permissionGroupWithUsageCountsEntries, category); 355 } 356 getAdvancedInfoSummaryString( Context context, List<Map.Entry<String, Integer>> permissionGroupWithUsageCounts)357 private CharSequence getAdvancedInfoSummaryString( 358 Context context, List<Map.Entry<String, Integer>> permissionGroupWithUsageCounts) { 359 int size = permissionGroupWithUsageCounts.size(); 360 if (size <= PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1) { 361 return ""; 362 } 363 364 // case for 1 extra item in the advanced info 365 if (size == PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT) { 366 String permGroupName = 367 permissionGroupWithUsageCounts 368 .get(PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1) 369 .getKey(); 370 return mViewModel.getPermissionGroupLabel(context, permGroupName); 371 } 372 373 String permGroupName1 = 374 permissionGroupWithUsageCounts 375 .get(PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1) 376 .getKey(); 377 String permGroupName2 = 378 permissionGroupWithUsageCounts 379 .get(PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT) 380 .getKey(); 381 CharSequence permGroupLabel1 = mViewModel.getPermissionGroupLabel(context, permGroupName1); 382 CharSequence permGroupLabel2 = mViewModel.getPermissionGroupLabel(context, permGroupName2); 383 384 // case for 2 extra items in the advanced info 385 if (size == PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT + 1) { 386 return context.getResources() 387 .getString( 388 R.string.perm_usage_adv_info_summary_2_items, 389 permGroupLabel1, 390 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() 396 .getString( 397 R.string.perm_usage_adv_info_summary_more_items, 398 permGroupLabel1, 399 permGroupLabel2, 400 numExtraItems); 401 } 402 403 /** Add preferences for permission usages. */ addUIContent( Context context, List<Map.Entry<String, Integer>> permissionGroupWithUsageCounts, PreferenceCategory category)404 private void addUIContent( 405 Context context, 406 List<Map.Entry<String, Integer>> permissionGroupWithUsageCounts, 407 PreferenceCategory category) { 408 boolean showSystem = mViewModel.getShowSystemApps(); 409 boolean show7Days = mViewModel.getShow7DaysData(); 410 411 for (int i = 0; i < permissionGroupWithUsageCounts.size(); i++) { 412 Map.Entry<String, Integer> permissionGroupWithUsageCount = 413 permissionGroupWithUsageCounts.get(i); 414 PermissionUsageControlPreference permissionUsagePreference = 415 new PermissionUsageControlPreference( 416 context, 417 permissionGroupWithUsageCount.getKey(), 418 permissionGroupWithUsageCount.getValue(), 419 showSystem, 420 mSessionId, 421 show7Days); 422 category.addPreference(permissionUsagePreference); 423 } 424 425 setLoading(false, true); 426 } 427 } 428