1 /* 2 * Copyright (C) 2015 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; 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.APP_PERMISSIONS_FRAGMENT_VIEWED; 22 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED; 23 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND; 24 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__DENIED; 25 import static com.android.permissioncontroller.permission.ui.handheld.UtilsKt.pressBack; 26 27 import android.app.ActionBar; 28 import android.app.Activity; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.graphics.drawable.Drawable; 32 import android.icu.text.ListFormatter; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.UserHandle; 36 import android.provider.Settings; 37 import android.util.Log; 38 import android.view.LayoutInflater; 39 import android.view.Menu; 40 import android.view.MenuInflater; 41 import android.view.MenuItem; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.widget.Toast; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.StringRes; 48 import androidx.lifecycle.ViewModelProvider; 49 import androidx.preference.Preference; 50 import androidx.preference.PreferenceCategory; 51 import androidx.preference.PreferenceScreen; 52 import androidx.preference.SwitchPreference; 53 54 import com.android.permissioncontroller.PermissionControllerStatsLog; 55 import com.android.permissioncontroller.R; 56 import com.android.permissioncontroller.permission.model.livedatatypes.AutoRevokeState; 57 import com.android.permissioncontroller.permission.ui.Category; 58 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel; 59 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel.GroupUiInfo; 60 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel.PermSubtitle; 61 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModelFactory; 62 import com.android.permissioncontroller.permission.utils.KotlinUtils; 63 import com.android.permissioncontroller.permission.utils.Utils; 64 import com.android.settingslib.HelpUtils; 65 66 import java.text.Collator; 67 import java.util.ArrayList; 68 import java.util.List; 69 import java.util.Map; 70 import java.util.Random; 71 72 /** 73 * Show and manage permission groups for an app. 74 * 75 * <p>Shows the list of permission groups the app has requested at one permission for. 76 */ 77 public final class AppPermissionGroupsFragment extends SettingsWithLargeHeader { 78 79 private static final String LOG_TAG = AppPermissionGroupsFragment.class.getSimpleName(); 80 private static final String IS_SYSTEM_PERMS_SCREEN = "_is_system_screen"; 81 private static final String AUTO_REVOKE_CATEGORY_KEY = "_AUTO_REVOKE_KEY"; 82 private static final String AUTO_REVOKE_SWITCH_KEY = "_AUTO_REVOKE_SWITCH_KEY"; 83 private static final String AUTO_REVOKE_SUMMARY_KEY = "_AUTO_REVOKE_SUMMARY_KEY"; 84 85 static final String EXTRA_HIDE_INFO_BUTTON = "hideInfoButton"; 86 87 private AppPermissionGroupsViewModel mViewModel; 88 private boolean mIsSystemPermsScreen; 89 private boolean mIsFirstLoad; 90 private String mPackageName; 91 private UserHandle mUser; 92 93 private Collator mCollator; 94 95 /** 96 * Create a bundle with the arguments needed by this fragment 97 * 98 * @param packageName The name of the package 99 * @param userHandle The user of this package 100 * @param sessionId The current session ID 101 * @param isSystemPermsScreen Whether or not this screen is the system permission screen, or 102 * the extra permissions screen 103 * 104 * @return A bundle with all of the args placed 105 */ createArgs(@onNull String packageName, @NonNull UserHandle userHandle, long sessionId, boolean isSystemPermsScreen)106 public static Bundle createArgs(@NonNull String packageName, @NonNull UserHandle userHandle, 107 long sessionId, boolean isSystemPermsScreen) { 108 Bundle arguments = new Bundle(); 109 arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName); 110 arguments.putParcelable(Intent.EXTRA_USER, userHandle); 111 arguments.putLong(EXTRA_SESSION_ID, sessionId); 112 arguments.putBoolean(IS_SYSTEM_PERMS_SCREEN, isSystemPermsScreen); 113 return arguments; 114 } 115 116 /** 117 * Create a bundle for a system permissions fragment 118 * 119 * @param packageName The name of the package 120 * @param userHandle The user of this package 121 * @param sessionId The current session ID 122 * 123 * @return A bundle with all of the args placed 124 */ createArgs(@onNull String packageName, @NonNull UserHandle userHandle, long sessionId)125 public static Bundle createArgs(@NonNull String packageName, @NonNull UserHandle userHandle, 126 long sessionId) { 127 return createArgs(packageName, userHandle, sessionId, true); 128 } 129 130 @Override onCreate(Bundle savedInstanceState)131 public void onCreate(Bundle savedInstanceState) { 132 super.onCreate(savedInstanceState); 133 setHasOptionsMenu(true); 134 mIsFirstLoad = true; 135 final ActionBar ab = getActivity().getActionBar(); 136 if (ab != null) { 137 ab.setDisplayHomeAsUpEnabled(true); 138 } 139 140 mPackageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); 141 mUser = getArguments().getParcelable(Intent.EXTRA_USER); 142 mIsSystemPermsScreen = getArguments().getBoolean(IS_SYSTEM_PERMS_SCREEN, true); 143 144 AppPermissionGroupsViewModelFactory factory = 145 new AppPermissionGroupsViewModelFactory(mPackageName, mUser, 146 getArguments().getLong(EXTRA_SESSION_ID, 0)); 147 148 mViewModel = new ViewModelProvider(this, factory).get(AppPermissionGroupsViewModel.class); 149 mViewModel.getPackagePermGroupsLiveData().observe(this, this::updatePreferences); 150 mViewModel.getAutoRevokeLiveData().observe(this, this::setAutoRevokeToggleState); 151 152 mCollator = Collator.getInstance( 153 getContext().getResources().getConfiguration().getLocales().get(0)); 154 155 if (mViewModel.getPackagePermGroupsLiveData().getValue() != null) { 156 updatePreferences(mViewModel.getPackagePermGroupsLiveData().getValue()); 157 } 158 } 159 160 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)161 public View onCreateView(LayoutInflater inflater, ViewGroup container, 162 Bundle savedInstanceState) { 163 getActivity().setTitle(R.string.app_permissions); 164 return super.onCreateView(inflater, container, savedInstanceState); 165 } 166 167 @Override onOptionsItemSelected(MenuItem item)168 public boolean onOptionsItemSelected(MenuItem item) { 169 switch (item.getItemId()) { 170 case android.R.id.home: { 171 pressBack(this); 172 return true; 173 } 174 175 case MENU_ALL_PERMS: { 176 mViewModel.showAllPermissions(this, AllAppPermissionsFragment.createArgs( 177 mPackageName, mUser)); 178 return true; 179 } 180 } 181 return super.onOptionsItemSelected(item); 182 } 183 184 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)185 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 186 super.onCreateOptionsMenu(menu, inflater); 187 if (mIsSystemPermsScreen) { 188 menu.add(Menu.NONE, MENU_ALL_PERMS, Menu.NONE, R.string.all_permissions); 189 HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_app_permissions, 190 getClass().getName()); 191 } 192 } 193 bindUi(SettingsWithLargeHeader fragment, String packageName, UserHandle user)194 private static void bindUi(SettingsWithLargeHeader fragment, String packageName, 195 UserHandle user) { 196 Activity activity = fragment.getActivity(); 197 Intent infoIntent = null; 198 if (!activity.getIntent().getBooleanExtra(EXTRA_HIDE_INFO_BUTTON, false)) { 199 infoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 200 .setData(Uri.fromParts("package", packageName, null)); 201 } 202 203 Drawable icon = KotlinUtils.INSTANCE.getBadgedPackageIcon(activity.getApplication(), 204 packageName, user); 205 fragment.setHeader(icon, KotlinUtils.INSTANCE.getPackageLabel(activity.getApplication(), 206 packageName, user), infoIntent, user, false); 207 208 } 209 createPreferenceScreenIfNeeded()210 private void createPreferenceScreenIfNeeded() { 211 if (getPreferenceScreen() == null) { 212 addPreferencesFromResource(R.xml.allowed_denied); 213 addAutoRevokePreferences(getPreferenceScreen()); 214 bindUi(this, mPackageName, mUser); 215 } 216 } 217 updatePreferences(Map<Category, List<GroupUiInfo>> groupMap)218 private void updatePreferences(Map<Category, List<GroupUiInfo>> groupMap) { 219 createPreferenceScreenIfNeeded(); 220 221 Context context = getPreferenceManager().getContext(); 222 if (context == null) { 223 return; 224 } 225 226 if (groupMap == null && mViewModel.getPackagePermGroupsLiveData().isInitialized()) { 227 Toast.makeText( 228 getActivity(), R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show(); 229 Log.w(LOG_TAG, "invalid package " + mPackageName); 230 231 pressBack(this); 232 233 return; 234 } 235 236 findPreference(Category.ALLOWED_FOREGROUND.getCategoryName()).setVisible(false); 237 238 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); 239 240 for (Category grantCategory : groupMap.keySet()) { 241 PreferenceCategory category = findPreference(grantCategory.getCategoryName()); 242 int numExtraPerms = 0; 243 244 category.removeAll(); 245 246 if (grantCategory.equals(Category.ALLOWED_FOREGROUND)) { 247 category.setVisible(false); 248 category = findPreference(Category.ALLOWED.getCategoryName()); 249 } 250 251 if (grantCategory.equals(Category.ASK)) { 252 if (groupMap.get(grantCategory).size() == 0) { 253 category.setVisible(false); 254 } else { 255 category.setVisible(true); 256 } 257 } 258 259 for (GroupUiInfo groupInfo : groupMap.get(grantCategory)) { 260 String groupName = groupInfo.getGroupName(); 261 262 PermissionControlPreference preference = new PermissionControlPreference(context, 263 mPackageName, groupName, mUser, AppPermissionGroupsFragment.class.getName(), 264 sessionId, grantCategory.getCategoryName(), true); 265 preference.setTitle(KotlinUtils.INSTANCE.getPermGroupLabel(context, groupName)); 266 preference.setIcon(KotlinUtils.INSTANCE.getPermGroupIcon(context, groupName)); 267 preference.setKey(groupName); 268 switch (groupInfo.getSubtitle()) { 269 case FOREGROUND_ONLY: 270 preference.setSummary(R.string.permission_subtitle_only_in_foreground); 271 break; 272 case MEDIA_ONLY: 273 preference.setSummary(R.string.permission_subtitle_media_only); 274 break; 275 case ALL_FILES: 276 preference.setSummary(R.string.permission_subtitle_all_files); 277 break; 278 } 279 if (groupInfo.getSubtitle() == PermSubtitle.FOREGROUND_ONLY) { 280 preference.setSummary(R.string.permission_subtitle_only_in_foreground); 281 } 282 if (groupInfo.isSystem() == mIsSystemPermsScreen) { 283 category.addPreference(preference); 284 } else if (!groupInfo.isSystem()) { 285 numExtraPerms++; 286 } 287 } 288 289 int noPermsStringRes = grantCategory.equals(Category.DENIED) 290 ? R.string.no_permissions_denied : R.string.no_permissions_allowed; 291 292 if (numExtraPerms > 0) { 293 final Preference extraPerms = setUpCustomPermissionsScreen(context, numExtraPerms, 294 grantCategory.getCategoryName()); 295 category.addPreference(extraPerms); 296 } 297 298 if (category.getPreferenceCount() == 0) { 299 setNoPermissionPreference(category, noPermsStringRes, context); 300 } 301 302 KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreferences, false); 303 } 304 305 setAutoRevokeToggleState(mViewModel.getAutoRevokeLiveData().getValue()); 306 307 if (mIsFirstLoad) { 308 logAppPermissionGroupsFragmentView(); 309 mIsFirstLoad = false; 310 } 311 } 312 addAutoRevokePreferences(PreferenceScreen screen)313 private void addAutoRevokePreferences(PreferenceScreen screen) { 314 Context context = screen.getPreferenceManager().getContext(); 315 316 PreferenceCategory autoRevokeCategory = new PreferenceCategory(context); 317 autoRevokeCategory.setKey(AUTO_REVOKE_CATEGORY_KEY); 318 screen.addPreference(autoRevokeCategory); 319 320 SwitchPreference autoRevokeSwitch = new SwitchPreference(context); 321 autoRevokeSwitch.setOnPreferenceClickListener((preference) -> { 322 mViewModel.setAutoRevoke(autoRevokeSwitch.isChecked()); 323 return true; 324 }); 325 autoRevokeSwitch.setTitle(R.string.auto_revoke_label); 326 autoRevokeSwitch.setKey(AUTO_REVOKE_SWITCH_KEY); 327 autoRevokeCategory.addPreference(autoRevokeSwitch); 328 329 Preference autoRevokeSummary = new Preference(context); 330 autoRevokeSummary.setIcon(Utils.applyTint(getActivity(), R.drawable.ic_info_outline, 331 android.R.attr.colorControlNormal)); 332 autoRevokeSummary.setKey(AUTO_REVOKE_SUMMARY_KEY); 333 autoRevokeCategory.addPreference(autoRevokeSummary); 334 } 335 setAutoRevokeToggleState(AutoRevokeState state)336 private void setAutoRevokeToggleState(AutoRevokeState state) { 337 if (state == null || !mViewModel.getPackagePermGroupsLiveData().isInitialized() 338 || getListView() == null || getView() == null) { 339 return; 340 } 341 342 PreferenceCategory autoRevokeCategory = getPreferenceScreen() 343 .findPreference(AUTO_REVOKE_CATEGORY_KEY); 344 SwitchPreference autoRevokeSwitch = autoRevokeCategory.findPreference( 345 AUTO_REVOKE_SWITCH_KEY); 346 Preference autoRevokeSummary = autoRevokeCategory.findPreference(AUTO_REVOKE_SUMMARY_KEY); 347 348 if (!state.isEnabledGlobal() || !state.getShouldShowSwitch()) { 349 autoRevokeSwitch.setVisible(false); 350 autoRevokeSummary.setVisible(false); 351 return; 352 } 353 autoRevokeSwitch.setVisible(true); 354 autoRevokeSummary.setVisible(true); 355 autoRevokeSwitch.setChecked(state.isEnabledForApp()); 356 357 List<String> groupLabels = new ArrayList<>(); 358 for (String groupName : state.getRevocableGroupNames()) { 359 PreferenceCategory category = getPreferenceScreen().findPreference( 360 Category.ALLOWED.getCategoryName()); 361 Preference pref = category.findPreference(groupName); 362 if (pref != null) { 363 groupLabels.add(pref.getTitle().toString()); 364 } 365 } 366 367 groupLabels.sort(mCollator); 368 if (groupLabels.isEmpty()) { 369 autoRevokeSummary.setSummary(R.string.auto_revoke_summary); 370 } else { 371 autoRevokeSummary.setSummary(getString(R.string.auto_revoke_summary_with_permissions, 372 ListFormatter.getInstance().format(groupLabels))); 373 } 374 } 375 comparePreferences(Preference lhs, Preference rhs)376 private int comparePreferences(Preference lhs, Preference rhs) { 377 String additionalTitle = lhs.getContext().getString(R.string.additional_permissions); 378 if (lhs.getTitle().equals(additionalTitle)) { 379 return 1; 380 } else if (rhs.getTitle().equals(additionalTitle)) { 381 return -1; 382 } 383 return mCollator.compare(lhs.getTitle().toString(), 384 rhs.getTitle().toString()); 385 } 386 setUpCustomPermissionsScreen(Context context, int count, String category)387 private Preference setUpCustomPermissionsScreen(Context context, int count, String category) { 388 final Preference extraPerms = new Preference(context); 389 extraPerms.setIcon(Utils.applyTint(getActivity(), R.drawable.ic_toc, 390 android.R.attr.colorControlNormal)); 391 extraPerms.setTitle(R.string.additional_permissions); 392 extraPerms.setKey(extraPerms.getTitle() + category); 393 extraPerms.setOnPreferenceClickListener(preference -> { 394 mViewModel.showExtraPerms(this, AppPermissionGroupsFragment.createArgs( 395 mPackageName, mUser, getArguments().getLong(EXTRA_SESSION_ID), false)); 396 return true; 397 }); 398 extraPerms.setSummary(getResources().getQuantityString( 399 R.plurals.additional_permissions_more, count, count)); 400 return extraPerms; 401 } 402 setNoPermissionPreference(PreferenceCategory category, @StringRes int stringId, Context context)403 private void setNoPermissionPreference(PreferenceCategory category, @StringRes int stringId, 404 Context context) { 405 Preference empty = new Preference(context); 406 empty.setKey(getString(stringId)); 407 empty.setTitle(empty.getKey()); 408 empty.setSelectable(false); 409 category.addPreference(empty); 410 } 411 logAppPermissionGroupsFragmentView()412 private void logAppPermissionGroupsFragmentView() { 413 Context context = getPreferenceManager().getContext(); 414 if (context == null) { 415 return; 416 } 417 String permissionSubtitleOnlyInForeground = 418 context.getString(R.string.permission_subtitle_only_in_foreground); 419 420 421 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); 422 long viewId = new Random().nextLong(); 423 424 PreferenceCategory allowed = findPreference(Category.ALLOWED.getCategoryName()); 425 426 int numAllowed = allowed.getPreferenceCount(); 427 for (int i = 0; i < numAllowed; i++) { 428 Preference preference = allowed.getPreference(i); 429 430 if (preference.getTitle().equals(getString(R.string.no_permissions_allowed))) { 431 // R.string.no_permission_allowed was added to PreferenceCategory 432 continue; 433 } 434 435 int category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED; 436 if (preference.getSummary() != null 437 && permissionSubtitleOnlyInForeground.contentEquals(preference.getSummary())) { 438 category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND; 439 } 440 441 logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(), 442 category); 443 } 444 445 PreferenceCategory denied = findPreference(Category.DENIED.getCategoryName()); 446 447 int numDenied = denied.getPreferenceCount(); 448 for (int i = 0; i < numDenied; i++) { 449 Preference preference = denied.getPreference(i); 450 if (preference.getTitle().equals(getString(R.string.no_permissions_denied))) { 451 // R.string.no_permission_denied was added to PreferenceCategory 452 continue; 453 } 454 logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(), 455 APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__DENIED); 456 } 457 } 458 logAppPermissionsFragmentViewEntry( long sessionId, long viewId, String permissionGroupName, int category)459 private void logAppPermissionsFragmentViewEntry( 460 long sessionId, long viewId, String permissionGroupName, int category) { 461 462 Integer uid = KotlinUtils.INSTANCE.getPackageUid(getActivity().getApplication(), 463 mPackageName, mUser); 464 if (uid == null) { 465 return; 466 } 467 PermissionControllerStatsLog.write(APP_PERMISSIONS_FRAGMENT_VIEWED, sessionId, viewId, 468 permissionGroupName, uid, mPackageName, category); 469 Log.v(LOG_TAG, "AppPermissionFragment view logged with sessionId=" + sessionId + " viewId=" 470 + viewId + " permissionGroupName=" + permissionGroupName + " uid=" 471 + uid + " packageName=" 472 + mPackageName + " category=" + category); 473 } 474 } 475