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