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.television; 18 19 import static android.Manifest.permission_group.NOTIFICATIONS; 20 21 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID; 22 import static com.android.permissioncontroller.hibernation.HibernationPolicyKt.isHibernationEnabled; 23 24 import android.app.ActionBar; 25 import android.app.Activity; 26 import android.app.Application; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.PackageInfo; 30 import android.content.pm.PackageManager; 31 import android.graphics.drawable.Drawable; 32 import android.hardware.SensorPrivacyManager; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.UserHandle; 36 import android.provider.Settings; 37 import android.text.BidiFormatter; 38 import android.util.ArraySet; 39 import android.util.Log; 40 import android.view.Menu; 41 import android.view.MenuInflater; 42 import android.view.MenuItem; 43 import android.view.View; 44 import android.widget.Toast; 45 46 import androidx.lifecycle.ViewModelProvider; 47 import androidx.preference.Preference; 48 import androidx.preference.Preference.OnPreferenceClickListener; 49 import androidx.preference.PreferenceCategory; 50 import androidx.preference.PreferenceScreen; 51 import androidx.preference.PreferenceViewHolder; 52 import androidx.preference.SwitchPreference; 53 54 import com.android.modules.utils.build.SdkLevel; 55 import com.android.permissioncontroller.R; 56 import com.android.permissioncontroller.permission.model.AppPermissionGroup; 57 import com.android.permissioncontroller.permission.model.AppPermissions; 58 import com.android.permissioncontroller.permission.model.livedatatypes.HibernationSettingState; 59 import com.android.permissioncontroller.permission.ui.ReviewPermissionsActivity; 60 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel; 61 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModelFactory; 62 import com.android.permissioncontroller.permission.utils.KotlinUtils; 63 import com.android.permissioncontroller.permission.utils.LocationUtils; 64 import com.android.permissioncontroller.permission.utils.StringUtils; 65 import com.android.permissioncontroller.permission.utils.Utils; 66 import com.android.permissioncontroller.permission.utils.legacy.LegacySafetyNetLogger; 67 68 public final class AppPermissionsFragment extends SettingsWithHeader 69 implements OnPreferenceClickListener { 70 71 private static final String LOG_TAG = "ManagePermsFragment"; 72 73 static final String EXTRA_HIDE_INFO_BUTTON = "hideInfoButton"; 74 private static final String AUTO_REVOKE_SWITCH_KEY = "_AUTO_REVOKE_SWITCH_KEY"; 75 private static final String UNUSED_APPS_KEY = "_UNUSED_APPS_KEY"; 76 77 private static final int MENU_ALL_PERMS = 0; 78 79 private ArraySet<AppPermissionGroup> mToggledGroups; 80 private AppPermissionGroupsViewModel mViewModel; 81 private AppPermissions mAppPermissions; 82 private PreferenceScreen mExtraScreen; 83 84 private boolean mHasConfirmedRevoke; 85 86 private SensorPrivacyManager mSensorPrivacyManager; 87 private final SensorPrivacyManager.OnSensorPrivacyChangedListener mPrivacyChangedListener = 88 (sensor, enabled) -> { 89 mAppPermissions.refresh(); 90 setPreferencesCheckedState(); 91 }; 92 newInstance(String packageName, UserHandle user)93 public static AppPermissionsFragment newInstance(String packageName, UserHandle user) { 94 return setPackage(new AppPermissionsFragment(), packageName, user); 95 } 96 setPackage( T fragment, String packageName, UserHandle user)97 private static <T extends PermissionsFrameFragment> T setPackage( 98 T fragment, String packageName, UserHandle user) { 99 Bundle arguments = new Bundle(); 100 arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName); 101 arguments.putParcelable(Intent.EXTRA_USER, user); 102 fragment.setArguments(arguments); 103 return fragment; 104 } 105 106 @Override onCreate(Bundle savedInstanceState)107 public void onCreate(Bundle savedInstanceState) { 108 super.onCreate(savedInstanceState); 109 setLoading(true /* loading */, false /* animate */); 110 setHasOptionsMenu(true); 111 final ActionBar ab = getActivity().getActionBar(); 112 if (ab != null) { 113 ab.setDisplayHomeAsUpEnabled(true); 114 } 115 116 final String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); 117 final UserHandle user = getArguments().getParcelable(Intent.EXTRA_USER); 118 119 Activity activity = getActivity(); 120 PackageInfo packageInfo = getPackageInfo(activity, packageName); 121 if (packageName == null) { 122 Toast.makeText(activity, R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show(); 123 getActivity().finish(); 124 return; 125 } 126 127 mAppPermissions = new AppPermissions(activity, packageInfo, true, 128 () -> getActivity().finish()); 129 130 if (mAppPermissions.isReviewRequired()) { 131 Intent intent = new Intent(getActivity(), ReviewPermissionsActivity.class); 132 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); 133 intent.putExtra(Intent.EXTRA_USER, user); 134 startActivity(intent); 135 getActivity().finish(); 136 return; 137 } 138 139 if (SdkLevel.isAtLeastT()) { 140 mSensorPrivacyManager = getContext().getSystemService(SensorPrivacyManager.class); 141 } 142 } 143 144 @Override onResume()145 public void onResume() { 146 super.onResume(); 147 final String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); 148 final UserHandle user = getArguments().getParcelable(Intent.EXTRA_USER); 149 150 AppPermissionGroupsViewModelFactory factory = 151 new AppPermissionGroupsViewModelFactory(packageName, user, 0); 152 mViewModel = new ViewModelProvider(this, factory).get(AppPermissionGroupsViewModel.class); 153 mViewModel.getAutoRevokeLiveData().observe(this, this::setAutoRevokeToggleState); 154 155 mAppPermissions.refresh(); 156 loadPreferences(); 157 setPreferencesCheckedState(); 158 if (mSensorPrivacyManager != null) { 159 mSensorPrivacyManager.addSensorPrivacyListener(mPrivacyChangedListener); 160 } 161 } 162 163 @Override onOptionsItemSelected(MenuItem item)164 public boolean onOptionsItemSelected(MenuItem item) { 165 switch (item.getItemId()) { 166 case android.R.id.home: { 167 getActivity().finish(); 168 return true; 169 } 170 171 case MENU_ALL_PERMS: { 172 PermissionsFrameFragment frag = 173 AllAppPermissionsFragment.newInstance( 174 getArguments().getString(Intent.EXTRA_PACKAGE_NAME)); 175 getFragmentManager().beginTransaction() 176 .replace(android.R.id.content, frag) 177 .addToBackStack("AllPerms") 178 .commit(); 179 return true; 180 } 181 } 182 return super.onOptionsItemSelected(item); 183 } 184 185 @Override onViewCreated(View view, Bundle savedInstanceState)186 public void onViewCreated(View view, Bundle savedInstanceState) { 187 super.onViewCreated(view, savedInstanceState); 188 if (mAppPermissions != null) { 189 bindUi(this, 190 getArguments().getString(Intent.EXTRA_PACKAGE_NAME), 191 getArguments().getParcelable(Intent.EXTRA_USER), 192 R.string.app_permissions_decor_title); 193 } 194 } 195 196 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)197 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 198 super.onCreateOptionsMenu(menu, inflater); 199 menu.add(Menu.NONE, MENU_ALL_PERMS, Menu.NONE, R.string.all_permissions); 200 } 201 bindUi(SettingsWithHeader fragment, String packageName, UserHandle user, int decorTitleStringResId)202 static void bindUi(SettingsWithHeader fragment, String packageName, 203 UserHandle user, int decorTitleStringResId) { 204 final Activity activity = fragment.getActivity(); 205 final Application application = activity.getApplication(); 206 207 CharSequence label = BidiFormatter.getInstance().unicodeWrap( 208 KotlinUtils.INSTANCE.getPackageLabel(application, packageName, user)); 209 Drawable icon= KotlinUtils.INSTANCE.getBadgedPackageIcon(application, packageName, user); 210 211 Intent infoIntent = null; 212 if (!activity.getIntent().getBooleanExtra(EXTRA_HIDE_INFO_BUTTON, false)) { 213 infoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 214 .setData(Uri.fromParts("package", packageName, null)); 215 } 216 217 fragment.setHeader(icon, label, infoIntent, fragment.getString( 218 R.string.additional_permissions_decor_title)); 219 } 220 loadPreferences()221 private void loadPreferences() { 222 Context context = getPreferenceManager().getContext(); 223 if (context == null) { 224 return; 225 } 226 227 PreferenceScreen screen = getPreferenceScreen(); 228 screen.removeAll(); 229 screen.addPreference(createHeaderLineTwoPreference(context)); 230 231 if (mExtraScreen != null) { 232 mExtraScreen.removeAll(); 233 mExtraScreen = null; 234 } 235 236 final Preference extraPerms = new Preference(context); 237 extraPerms.setIcon(R.drawable.ic_toc); 238 extraPerms.setTitle(R.string.additional_permissions); 239 240 for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { 241 if (!Utils.shouldShowPermission(getContext(), group) 242 || group.getName().equals(NOTIFICATIONS)) { 243 // Skip notification group on TV 244 continue; 245 } 246 247 boolean isPlatform = group.getDeclaringPackage().equals(Utils.OS_PKG); 248 249 Preference preference = new Preference(context); 250 preference.setOnPreferenceClickListener(this); 251 preference.setKey(group.getName()); 252 Drawable icon = Utils.loadDrawable(context.getPackageManager(), 253 group.getIconPkg(), group.getIconResId()); 254 preference.setIcon(Utils.applyTint(getContext(), icon, 255 android.R.attr.colorControlNormal)); 256 preference.setTitle(group.getLabel()); 257 if (group.isSystemFixed()) { 258 preference.setSummary(getString(R.string.permission_summary_enabled_system_fixed)); 259 } else if (group.isPolicyFixed()) { 260 preference.setSummary(getString(R.string.permission_summary_enforced_by_policy)); 261 } 262 preference.setPersistent(false); 263 preference.setEnabled(!group.isSystemFixed() && !group.isPolicyFixed()); 264 265 if (isPlatform) { 266 screen.addPreference(preference); 267 } else { 268 if (mExtraScreen == null) { 269 mExtraScreen = getPreferenceManager().createPreferenceScreen(context); 270 mExtraScreen.addPreference(createHeaderLineTwoPreference(context)); 271 } 272 mExtraScreen.addPreference(preference); 273 } 274 } 275 276 final String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); 277 final UserHandle user = getArguments().getParcelable(Intent.EXTRA_USER); 278 279 if (mExtraScreen != null) { 280 extraPerms.setOnPreferenceClickListener(preference -> { 281 AdditionalPermissionsFragment frag = new AdditionalPermissionsFragment(); 282 setPackage(frag, packageName, user); 283 frag.setTargetFragment(AppPermissionsFragment.this, 0); 284 getFragmentManager().beginTransaction() 285 .replace(android.R.id.content, frag) 286 .addToBackStack(null) 287 .commit(); 288 return true; 289 }); 290 int count = mExtraScreen.getPreferenceCount() - 1; 291 extraPerms.setSummary(StringUtils.getIcuPluralsString(getContext(), 292 R.string.additional_permissions_more, count)); 293 screen.addPreference(extraPerms); 294 } 295 296 addAutoRevokePreferences(getPreferenceScreen()); 297 298 setLoading(false /* loading */, true /* animate */); 299 } 300 301 /** 302 * Creates a heading below decor_title and above the rest of the preferences. This heading 303 * displays the app name and banner icon. It's used in both system and additional permissions 304 * fragments for each app. The styling used is the same as a leanback preference with a 305 * customized background color 306 * @param context The context the preferences created on 307 * @return The preference header to be inserted as the first preference in the list. 308 */ createHeaderLineTwoPreference(Context context)309 private Preference createHeaderLineTwoPreference(Context context) { 310 Preference headerLineTwo = new Preference(context) { 311 @Override 312 public void onBindViewHolder(PreferenceViewHolder holder) { 313 super.onBindViewHolder(holder); 314 holder.itemView.setBackgroundColor( 315 getResources().getColor(R.color.lb_header_banner_color)); 316 } 317 }; 318 headerLineTwo.setKey(HEADER_PREFERENCE_KEY); 319 headerLineTwo.setSelectable(false); 320 headerLineTwo.setTitle(mLabel); 321 headerLineTwo.setIcon(mIcon); 322 return headerLineTwo; 323 } 324 325 @Override onPreferenceClick(final Preference preference)326 public boolean onPreferenceClick(final Preference preference) { 327 String groupName = preference.getKey(); 328 final AppPermissionGroup group = mAppPermissions.getPermissionGroup(groupName); 329 330 if (group == null) { 331 return false; 332 } 333 334 addToggledGroup(group); 335 336 if (LocationUtils.isLocationGroupAndProvider(getContext(), group.getName(), 337 group.getApp().packageName)) { 338 LocationUtils.showLocationDialog(getContext(), mAppPermissions.getAppLabel()); 339 return false; 340 } 341 342 AppPermissionFragment frag = new AppPermissionFragment(); 343 344 frag.setArguments(AppPermissionFragment.createArgs( 345 /* packageName= */ group.getApp().packageName, 346 /* permName= */ null, 347 /* groupName= */ group.getName(), 348 /* userHandle= */ group.getUser(), 349 /* caller= */ null, 350 /* sessionId= */ INVALID_SESSION_ID, 351 /* grantCategory= */ null)); 352 frag.setTargetFragment(AppPermissionsFragment.this, 0); 353 getFragmentManager().beginTransaction() 354 .replace(android.R.id.content, frag) 355 .addToBackStack(null) 356 .commit(); 357 358 return true; 359 } 360 361 @Override onPause()362 public void onPause() { 363 mViewModel.getAutoRevokeLiveData().removeObservers(this); 364 super.onPause(); 365 logToggledGroups(); 366 if (mSensorPrivacyManager != null) { 367 mSensorPrivacyManager.removeSensorPrivacyListener(mPrivacyChangedListener); 368 } 369 } 370 addToggledGroup(AppPermissionGroup group)371 private void addToggledGroup(AppPermissionGroup group) { 372 if (mToggledGroups == null) { 373 mToggledGroups = new ArraySet<>(); 374 } 375 mToggledGroups.add(group); 376 } 377 logToggledGroups()378 private void logToggledGroups() { 379 if (mToggledGroups != null) { 380 LegacySafetyNetLogger.logPermissionsToggled(mToggledGroups); 381 mToggledGroups = null; 382 } 383 } 384 setPreferencesCheckedState()385 private void setPreferencesCheckedState() { 386 setPreferencesCheckedState(getPreferenceScreen()); 387 if (mExtraScreen != null) { 388 setPreferencesCheckedState(mExtraScreen); 389 } 390 setAutoRevokeToggleState(mViewModel.getAutoRevokeLiveData().getValue()); 391 } 392 setPreferencesCheckedState(PreferenceScreen screen)393 private void setPreferencesCheckedState(PreferenceScreen screen) { 394 int preferenceCount = screen.getPreferenceCount(); 395 for (int i = 0; i < preferenceCount; i++) { 396 Preference preference = screen.getPreference(i); 397 if (preference.getKey() == null) { 398 continue; 399 } 400 AppPermissionGroup group = mAppPermissions.getPermissionGroup(preference.getKey()); 401 if (group == null) { 402 continue; 403 } 404 AppPermissionGroup backgroundGroup = group.getBackgroundPermissions(); 405 406 if (group.areRuntimePermissionsGranted()) { 407 if (backgroundGroup == null) { 408 preference.setSummary(R.string.app_permission_button_allow); 409 } else { 410 if (backgroundGroup.areRuntimePermissionsGranted()) { 411 preference.setSummary(R.string.permission_access_always); 412 } else { 413 preference.setSummary(R.string.permission_access_only_foreground); 414 } 415 } 416 } else { 417 if (group.isOneTime()) { 418 preference.setSummary(R.string.app_permission_button_ask); 419 } else { 420 preference.setSummary(R.string.permission_access_never); 421 } 422 } 423 } 424 } 425 426 addAutoRevokePreferences(PreferenceScreen screen)427 private void addAutoRevokePreferences(PreferenceScreen screen) { 428 SwitchPreference autoRevokeSwitch = 429 new SwitchPreference(screen.getPreferenceManager().getContext()); 430 autoRevokeSwitch.setLayoutResource(R.layout.preference_permissions_revoke); 431 autoRevokeSwitch.setOnPreferenceClickListener((preference) -> { 432 mViewModel.setAutoRevoke(autoRevokeSwitch.isChecked()); 433 android.util.Log.w(LOG_TAG, "setAutoRevoke " + autoRevokeSwitch.isChecked()); 434 return true; 435 }); 436 autoRevokeSwitch.setTitle(isHibernationEnabled() ? R.string.unused_apps_label 437 : R.string.auto_revoke_label); 438 autoRevokeSwitch.setSummary(R.string.auto_revoke_summary); 439 autoRevokeSwitch.setKey(AUTO_REVOKE_SWITCH_KEY); 440 if (isHibernationEnabled()) { 441 PreferenceCategory unusedAppsCategory = new PreferenceCategory( 442 screen.getPreferenceManager().getContext()); 443 unusedAppsCategory.setKey(UNUSED_APPS_KEY); 444 unusedAppsCategory.setTitle(R.string.unused_apps); 445 screen.addPreference(unusedAppsCategory); 446 unusedAppsCategory.addPreference(autoRevokeSwitch); 447 } else { 448 screen.addPreference(autoRevokeSwitch); 449 } 450 } 451 setAutoRevokeToggleState(HibernationSettingState state)452 private void setAutoRevokeToggleState(HibernationSettingState state) { 453 SwitchPreference autoRevokeSwitch = getPreferenceScreen().findPreference( 454 AUTO_REVOKE_SWITCH_KEY); 455 if (state == null || autoRevokeSwitch == null) { 456 return; 457 } 458 if (state.getRevocableGroupNames().isEmpty()) { 459 if (isHibernationEnabled()) { 460 getPreferenceScreen().findPreference(UNUSED_APPS_KEY).setVisible(false); 461 } 462 autoRevokeSwitch.setVisible(false); 463 return; 464 } 465 if (isHibernationEnabled()) { 466 getPreferenceScreen().findPreference(UNUSED_APPS_KEY).setVisible(true); 467 } 468 autoRevokeSwitch.setVisible(true); 469 autoRevokeSwitch.setChecked(state.isEligibleForHibernation()); 470 } 471 getPackageInfo(Activity activity, String packageName)472 private static PackageInfo getPackageInfo(Activity activity, String packageName) { 473 try { 474 return activity.getPackageManager().getPackageInfo( 475 packageName, PackageManager.GET_PERMISSIONS); 476 } catch (PackageManager.NameNotFoundException e) { 477 Log.i(LOG_TAG, "No package:" + activity.getCallingPackage(), e); 478 return null; 479 } 480 } 481 482 public static class AdditionalPermissionsFragment extends SettingsWithHeader { 483 AppPermissionsFragment mOuterFragment; 484 485 @Override onCreate(Bundle savedInstanceState)486 public void onCreate(Bundle savedInstanceState) { 487 mOuterFragment = (AppPermissionsFragment) getTargetFragment(); 488 super.onCreate(savedInstanceState); 489 setHasOptionsMenu(true); 490 } 491 492 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)493 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 494 setPreferenceScreen(mOuterFragment.mExtraScreen); 495 } 496 497 @Override onViewCreated(View view, Bundle savedInstanceState)498 public void onViewCreated(View view, Bundle savedInstanceState) { 499 super.onViewCreated(view, savedInstanceState); 500 bindUi(this, 501 getArguments().getString(Intent.EXTRA_PACKAGE_NAME), 502 getArguments().getParcelable(Intent.EXTRA_USER), 503 R.string.additional_permissions_decor_title); 504 } 505 506 @Override onOptionsItemSelected(MenuItem item)507 public boolean onOptionsItemSelected(MenuItem item) { 508 switch (item.getItemId()) { 509 case android.R.id.home: 510 getFragmentManager().popBackStack(); 511 return true; 512 } 513 return super.onOptionsItemSelected(item); 514 } 515 } 516 } 517