1 /* 2 * Copyright (C) 2020 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.car.settings.applications; 18 19 import static android.app.Activity.RESULT_OK; 20 21 import static com.android.car.settings.applications.ApplicationsUtils.isKeepEnabledPackage; 22 import static com.android.car.settings.applications.ApplicationsUtils.isProfileOrDeviceOwner; 23 import static com.android.car.settings.common.ActionButtonsPreference.ActionButtons; 24 import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG; 25 26 import android.app.Activity; 27 import android.app.ActivityManager; 28 import android.app.admin.DevicePolicyManager; 29 import android.car.drivingstate.CarUxRestrictions; 30 import android.content.BroadcastReceiver; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.pm.ApplicationInfo; 35 import android.content.pm.PackageInfo; 36 import android.content.pm.PackageManager; 37 import android.content.pm.ResolveInfo; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.os.UserHandle; 41 import android.os.UserManager; 42 import android.util.ArraySet; 43 import android.view.View; 44 45 import androidx.annotation.Nullable; 46 import androidx.annotation.VisibleForTesting; 47 48 import com.android.car.settings.R; 49 import com.android.car.settings.common.ActionButtonInfo; 50 import com.android.car.settings.common.ActionButtonsPreference; 51 import com.android.car.settings.common.ActivityResultCallback; 52 import com.android.car.settings.common.ConfirmationDialogFragment; 53 import com.android.car.settings.common.FragmentController; 54 import com.android.car.settings.common.Logger; 55 import com.android.car.settings.common.PreferenceController; 56 import com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment; 57 import com.android.car.settings.enterprise.DeviceAdminAddActivity; 58 import com.android.car.settings.profiles.ProfileHelper; 59 import com.android.settingslib.Utils; 60 import com.android.settingslib.applications.ApplicationsState; 61 62 import java.util.ArrayList; 63 import java.util.Arrays; 64 import java.util.List; 65 import java.util.Set; 66 67 /** 68 * Shows actions associated with an application, like uninstall and forceStop. 69 * 70 * <p>To uninstall an app, it must <i>not</i> be: 71 * <ul> 72 * <li>a system bundled app 73 * <li>system signed 74 * <li>managed by an active admin from a device policy 75 * <li>a device or profile owner 76 * <li>the only home app 77 * <li>the default home app 78 * <li>for a user with the {@link UserManager#DISALLOW_APPS_CONTROL} restriction 79 * <li>for a user with the {@link UserManager#DISALLOW_UNINSTALL_APPS} restriction 80 * </ul> 81 * 82 * <p>For apps that cannot be uninstalled, a disable option is shown instead (or enable if the app 83 * is already disabled). 84 */ 85 public class ApplicationActionButtonsPreferenceController extends 86 PreferenceController<ActionButtonsPreference> implements ActivityResultCallback { 87 private static final Logger LOG = new Logger( 88 ApplicationActionButtonsPreferenceController.class); 89 90 private static final List<String> FORCE_STOP_RESTRICTIONS = 91 Arrays.asList(UserManager.DISALLOW_APPS_CONTROL); 92 private static final List<String> UNINSTALL_RESTRICTIONS = 93 Arrays.asList(UserManager.DISALLOW_UNINSTALL_APPS, UserManager.DISALLOW_APPS_CONTROL); 94 private static final List<String> DISABLE_RESTRICTIONS = 95 Arrays.asList(UserManager.DISALLOW_APPS_CONTROL); 96 97 @VisibleForTesting 98 static final String DISABLE_CONFIRM_DIALOG_TAG = 99 "com.android.car.settings.applications.DisableConfirmDialog"; 100 @VisibleForTesting 101 static final String FORCE_STOP_CONFIRM_DIALOG_TAG = 102 "com.android.car.settings.applications.ForceStopConfirmDialog"; 103 104 @VisibleForTesting 105 static final int UNINSTALL_REQUEST_CODE = 10; 106 107 @VisibleForTesting 108 static final int UNINSTALL_DEVICE_ADMIN_REQUEST_CODE = 11; 109 110 private DevicePolicyManager mDpm; 111 private PackageManager mPm; 112 private UserManager mUserManager; 113 private ProfileHelper mProfileHelper; 114 private ApplicationsState.Session mSession; 115 116 private ApplicationsState.AppEntry mAppEntry; 117 private ApplicationsState mApplicationsState; 118 private String mPackageName; 119 private PackageInfo mPackageInfo; 120 121 private String mRestriction; 122 123 @VisibleForTesting 124 final ConfirmationDialogFragment.ConfirmListener mForceStopConfirmListener = 125 new ConfirmationDialogFragment.ConfirmListener() { 126 @Override 127 public void onConfirm(@Nullable Bundle arguments) { 128 LOG.d("Stopping package " + mPackageName); 129 getContext().getSystemService(ActivityManager.class) 130 .forceStopPackage(mPackageName); 131 int userId = UserHandle.getUserId(mAppEntry.info.uid); 132 mApplicationsState.invalidatePackage(mPackageName, userId); 133 } 134 }; 135 136 private final View.OnClickListener mForceStopClickListener = i -> { 137 if (ignoreActionBecauseItsDisabledByAdmin(FORCE_STOP_RESTRICTIONS)) return; 138 ConfirmationDialogFragment dialogFragment = 139 new ConfirmationDialogFragment.Builder(getContext()) 140 .setTitle(R.string.force_stop_dialog_title) 141 .setMessage(R.string.force_stop_dialog_text) 142 .setPositiveButton(android.R.string.ok, 143 mForceStopConfirmListener) 144 .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null) 145 .build(); 146 getFragmentController().showDialog(dialogFragment, FORCE_STOP_CONFIRM_DIALOG_TAG); 147 }; 148 149 @VisibleForTesting 150 final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() { 151 @Override 152 public void onReceive(Context context, Intent intent) { 153 boolean enabled = getResultCode() != Activity.RESULT_CANCELED; 154 LOG.d("Got broadcast response: Restart status for " + mPackageName + " " + enabled); 155 updateForceStopButtonInner(enabled); 156 } 157 }; 158 159 @VisibleForTesting 160 final ConfirmationDialogFragment.ConfirmListener mDisableConfirmListener = i -> { 161 mPm.setApplicationEnabledSetting(mPackageName, 162 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, /* flags= */ 0); 163 updateUninstallButtonInner(false); 164 }; 165 166 private final View.OnClickListener mDisableClickListener = i -> { 167 if (ignoreActionBecauseItsDisabledByAdmin(DISABLE_RESTRICTIONS)) return; 168 ConfirmationDialogFragment dialogFragment = 169 new ConfirmationDialogFragment.Builder(getContext()) 170 .setMessage(getContext().getString(R.string.app_disable_dialog_text)) 171 .setPositiveButton(R.string.app_disable_dialog_positive, 172 mDisableConfirmListener) 173 .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null) 174 .build(); 175 getFragmentController().showDialog(dialogFragment, DISABLE_CONFIRM_DIALOG_TAG); 176 }; 177 178 private final View.OnClickListener mEnableClickListener = i -> { 179 if (ignoreActionBecauseItsDisabledByAdmin(DISABLE_RESTRICTIONS)) return; 180 mPm.setApplicationEnabledSetting(mPackageName, 181 PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, /* flags= */ 0); 182 updateUninstallButtonInner(true); 183 }; 184 185 private final View.OnClickListener mUninstallClickListener = i -> { 186 if (ignoreActionBecauseItsDisabledByAdmin(UNINSTALL_RESTRICTIONS)) return; 187 Uri packageUri = Uri.parse("package:" + mPackageName); 188 if (mDpm.packageHasActiveAdmins(mPackageName)) { 189 // Show Device Admin app details screen to deactivate the device admin before it can 190 // be uninstalled. 191 Intent deviceAdminIntent = new Intent(getContext(), DeviceAdminAddActivity.class) 192 .putExtra(DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME, mPackageName); 193 getFragmentController().startActivityForResult(deviceAdminIntent, 194 UNINSTALL_DEVICE_ADMIN_REQUEST_CODE, /* callback= */ this); 195 } else { 196 Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); 197 uninstallIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); 198 getFragmentController().startActivityForResult(uninstallIntent, UNINSTALL_REQUEST_CODE, 199 /* callback= */ this); 200 } 201 }; 202 203 private final ApplicationsState.Callbacks mApplicationStateCallbacks = 204 new ApplicationsState.Callbacks() { 205 @Override 206 public void onRunningStateChanged(boolean running) { 207 } 208 209 @Override 210 public void onPackageListChanged() { 211 refreshUi(); 212 } 213 214 @Override 215 public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) { 216 } 217 218 @Override 219 public void onPackageIconChanged() { 220 } 221 222 @Override 223 public void onPackageSizeChanged(String packageName) { 224 } 225 226 @Override 227 public void onAllSizesComputed() { 228 } 229 230 @Override 231 public void onLauncherInfoChanged() { 232 } 233 234 @Override 235 public void onLoadEntriesCompleted() { 236 } 237 }; 238 ApplicationActionButtonsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)239 public ApplicationActionButtonsPreferenceController(Context context, String preferenceKey, 240 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 241 super(context, preferenceKey, fragmentController, uxRestrictions); 242 mDpm = context.getSystemService(DevicePolicyManager.class); 243 mPm = context.getPackageManager(); 244 mUserManager = UserManager.get(context); 245 mProfileHelper = ProfileHelper.getInstance(context); 246 } 247 248 @Override getPreferenceType()249 protected Class<ActionButtonsPreference> getPreferenceType() { 250 return ActionButtonsPreference.class; 251 } 252 253 /** Sets the {@link ApplicationsState.AppEntry} which is used to load the app name and icon. */ setAppEntry( ApplicationsState.AppEntry appEntry)254 public ApplicationActionButtonsPreferenceController setAppEntry( 255 ApplicationsState.AppEntry appEntry) { 256 mAppEntry = appEntry; 257 return this; 258 } 259 260 /** Sets the {@link ApplicationsState} which is used to load the app name and icon. */ setAppState( ApplicationsState applicationsState)261 public ApplicationActionButtonsPreferenceController setAppState( 262 ApplicationsState applicationsState) { 263 mApplicationsState = applicationsState; 264 return this; 265 } 266 267 /** 268 * Set the packageName, which is used to perform actions on a particular package. 269 */ setPackageName(String packageName)270 public ApplicationActionButtonsPreferenceController setPackageName(String packageName) { 271 mPackageName = packageName; 272 return this; 273 } 274 275 @Override checkInitialized()276 protected void checkInitialized() { 277 if (mAppEntry == null || mApplicationsState == null || mPackageName == null) { 278 throw new IllegalStateException( 279 "AppEntry, AppState, and PackageName should be set before calling this " 280 + "function"); 281 } 282 } 283 284 @Override onCreateInternal()285 protected void onCreateInternal() { 286 ConfirmationDialogFragment.resetListeners( 287 (ConfirmationDialogFragment) getFragmentController().findDialogByTag( 288 DISABLE_CONFIRM_DIALOG_TAG), 289 mDisableConfirmListener, 290 /* rejectListener= */ null, 291 /* neutralListener= */ null); 292 ConfirmationDialogFragment.resetListeners( 293 (ConfirmationDialogFragment) getFragmentController().findDialogByTag( 294 FORCE_STOP_CONFIRM_DIALOG_TAG), 295 mForceStopConfirmListener, 296 /* rejectListener= */ null, 297 /* neutralListener= */ null); 298 getPreference().getButton(ActionButtons.BUTTON2) 299 .setText(R.string.force_stop) 300 .setIcon(R.drawable.ic_warning) 301 .setOnClickListener(mForceStopClickListener) 302 .setEnabled(false); 303 mSession = mApplicationsState.newSession(mApplicationStateCallbacks); 304 } 305 306 @Override onStartInternal()307 protected void onStartInternal() { 308 mSession.onResume(); 309 } 310 311 @Override onStopInternal()312 protected void onStopInternal() { 313 mSession.onPause(); 314 } 315 316 @Override updateState(ActionButtonsPreference preference)317 protected void updateState(ActionButtonsPreference preference) { 318 refreshAppEntry(); 319 if (mAppEntry == null) { 320 getFragmentController().goBack(); 321 return; 322 } 323 updateForceStopButton(); 324 updateUninstallButton(); 325 } 326 refreshAppEntry()327 private void refreshAppEntry() { 328 mAppEntry = mApplicationsState.getEntry(mPackageName, UserHandle.myUserId()); 329 if (mAppEntry != null) { 330 try { 331 mPackageInfo = mPm.getPackageInfo(mPackageName, 332 PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_ANY_USER 333 | PackageManager.GET_SIGNATURES | PackageManager.GET_PERMISSIONS); 334 } catch (PackageManager.NameNotFoundException e) { 335 LOG.e("Exception when retrieving package:" + mPackageName, e); 336 mPackageInfo = null; 337 } 338 } else { 339 mPackageInfo = null; 340 } 341 } 342 updateForceStopButton()343 private void updateForceStopButton() { 344 if (mDpm.packageHasActiveAdmins(mPackageName)) { 345 updateForceStopButtonInner(/* enabled= */ false); 346 } else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_STOPPED) == 0) { 347 // If the app isn't explicitly stopped, then always show the force stop button. 348 updateForceStopButtonInner(/* enabled= */ true); 349 } else { 350 Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART, 351 Uri.fromParts("package", mPackageName, /* fragment= */ null)); 352 intent.putExtra(Intent.EXTRA_PACKAGES, new String[]{mPackageName}); 353 intent.putExtra(Intent.EXTRA_UID, mAppEntry.info.uid); 354 intent.putExtra(Intent.EXTRA_USER_HANDLE, 355 UserHandle.getUserId(mAppEntry.info.uid)); 356 LOG.d("Sending broadcast to query restart status for " + mPackageName); 357 getContext().sendOrderedBroadcastAsUser(intent, 358 UserHandle.CURRENT, 359 /* receiverPermission= */ null, 360 mCheckKillProcessesReceiver, 361 /* scheduler= */ null, 362 Activity.RESULT_CANCELED, 363 /* initialData= */ null, 364 /* initialExtras= */ null); 365 } 366 } 367 updateForceStopButtonInner(boolean enabled)368 private void updateForceStopButtonInner(boolean enabled) { 369 if (enabled) { 370 Boolean shouldDisable = shouldDisableButtonBecauseOfUserRestriction("Force Stop", 371 UserManager.DISALLOW_APPS_CONTROL); 372 if (shouldDisable != null) { 373 if (shouldDisable) { 374 enabled = false; 375 } else { 376 mRestriction = UserManager.DISALLOW_APPS_CONTROL; 377 } 378 } 379 } 380 381 getPreference().getButton(ActionButtons.BUTTON2).setEnabled(enabled); 382 } 383 updateUninstallButtonInner(boolean isAppEnabled)384 private void updateUninstallButtonInner(boolean isAppEnabled) { 385 ActionButtonInfo uninstallButton = getPreference().getButton(ActionButtons.BUTTON1); 386 if (isBundledApp()) { 387 if (isAppEnabled) { 388 uninstallButton.setText(R.string.disable_text).setIcon( 389 R.drawable.ic_block).setOnClickListener(mDisableClickListener); 390 } else { 391 uninstallButton.setText(R.string.enable_text).setIcon( 392 R.drawable.ic_check_circle).setOnClickListener(mEnableClickListener); 393 } 394 } else { 395 uninstallButton.setText(R.string.uninstall_text).setIcon( 396 R.drawable.ic_delete).setOnClickListener(mUninstallClickListener); 397 } 398 399 uninstallButton.setEnabled(!shouldDisableUninstallButton()); 400 } 401 updateUninstallButton()402 private void updateUninstallButton() { 403 updateUninstallButtonInner(isAppEnabled()); 404 } 405 shouldDisableUninstallButton()406 private boolean shouldDisableUninstallButton() { 407 if (shouldDisableUninstallForHomeApp()) { 408 LOG.d("Uninstall disabled for home app"); 409 return true; 410 } 411 412 if (isAppEnabled() && isKeepEnabledPackage(getContext(), mPackageName)) { 413 LOG.d("Disable button disabled for keep enabled package"); 414 return true; 415 } 416 417 if (Utils.isSystemPackage(getContext().getResources(), mPm, mPackageInfo)) { 418 LOG.d("Uninstall disabled for system package"); 419 return true; 420 } 421 422 // We don't allow uninstalling profile/device owner on any profile because if it's a system 423 // app, "uninstall" is actually "downgrade to the system version + disable", and 424 // "downgrade" will clear data on all profiles. 425 if (isProfileOrDeviceOwner(mPackageName, mDpm, mProfileHelper)) { 426 LOG.d("Uninstall disabled because package is profile or device owner"); 427 return true; 428 } 429 430 if (mDpm.isUninstallInQueue(mPackageName)) { 431 LOG.d("Uninstall disabled because intent is already queued"); 432 return true; 433 } 434 435 Boolean shouldDisable = shouldDisableButtonBecauseOfUserRestriction("Uninstall", 436 UserManager.DISALLOW_APPS_CONTROL); 437 if (shouldDisable != null) return shouldDisable; 438 439 shouldDisable = shouldDisableButtonBecauseOfUserRestriction("Uninstall", 440 UserManager.DISALLOW_UNINSTALL_APPS); 441 if (shouldDisable != null) return shouldDisable; 442 443 return false; 444 } 445 446 /** 447 * Checks whether a button should be disabled because the user has the given restriction 448 * (and whether the restriction was was set by a device admin). 449 * 450 * @param button action name (for logging purposes) 451 * @param restriction user restriction 452 * 453 * @return {@code null} if the user doesn't have the restriction, {@value Boolean#TRUE} if it 454 * should be disabled because of {@link UserManager} restrictions, or {@value Boolean#FALSE} if 455 * should not be disabled because of {@link DevicePolicyManager} restrictions (in which case 456 * {@link #mRestriction} is updated with the restriction). 457 */ 458 @Nullable shouldDisableButtonBecauseOfUserRestriction(String button, String restriction)459 private Boolean shouldDisableButtonBecauseOfUserRestriction(String button, String restriction) { 460 if (!mUserManager.hasUserRestriction(restriction)) return null; 461 462 UserHandle user = UserHandle.getUserHandleForUid(mAppEntry.info.uid); 463 464 if (mUserManager.hasBaseUserRestriction(restriction, user)) { 465 LOG.d(button + " disabled because " + user + " has " + restriction + " restriction"); 466 return Boolean.TRUE; 467 } 468 469 LOG.d(button + " NOT disabled because " + user + " has " + restriction + " restriction but " 470 + "it was set by a device admin (it will show a dialog explaining that instead)"); 471 mRestriction = restriction; 472 return Boolean.FALSE; 473 } 474 475 /** 476 * Returns {@code true} if the package is a Home app that should not be uninstalled. We don't 477 * risk downgrading bundled home apps because that can interfere with home-key resolution. We 478 * can't allow removal of the only home app, and we don't want to allow removal of an 479 * explicitly preferred home app. The user can go to Home settings and pick a different app, 480 * after which we'll permit removal of the now-not-default app. 481 */ shouldDisableUninstallForHomeApp()482 private boolean shouldDisableUninstallForHomeApp() { 483 Set<String> homePackages = new ArraySet<>(); 484 // Get list of "home" apps and trace through any meta-data references. 485 List<ResolveInfo> homeActivities = new ArrayList<>(); 486 ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities); 487 for (int i = 0; i < homeActivities.size(); i++) { 488 ResolveInfo ri = homeActivities.get(i); 489 String activityPkg = ri.activityInfo.packageName; 490 homePackages.add(activityPkg); 491 492 // Also make sure to include anything proxying for the home app. 493 Bundle metadata = ri.activityInfo.metaData; 494 if (metadata != null) { 495 String metaPkg = metadata.getString(ActivityManager.META_HOME_ALTERNATE); 496 if (signaturesMatch(metaPkg, activityPkg)) { 497 homePackages.add(metaPkg); 498 } 499 } 500 } 501 502 if (homePackages.contains(mPackageName)) { 503 if (isBundledApp()) { 504 // Don't risk a downgrade. 505 return true; 506 } else if (currentDefaultHome == null) { 507 // No preferred default. Permit uninstall only when there is more than one 508 // candidate. 509 return (homePackages.size() == 1); 510 } else { 511 // Explicit default home app. Forbid uninstall of that one, but permit it for 512 // installed-but-inactive ones. 513 return mPackageName.equals(currentDefaultHome.getPackageName()); 514 } 515 } else { 516 // Not a home app. 517 return false; 518 } 519 } 520 signaturesMatch(String pkg1, String pkg2)521 private boolean signaturesMatch(String pkg1, String pkg2) { 522 if (pkg1 != null && pkg2 != null) { 523 try { 524 int match = mPm.checkSignatures(pkg1, pkg2); 525 if (match >= PackageManager.SIGNATURE_MATCH) { 526 return true; 527 } 528 } catch (Exception e) { 529 // e.g. package not found during lookup. Possibly bad input. 530 // Just return false as this isn't a reason to crash given the use case. 531 } 532 } 533 return false; 534 } 535 isBundledApp()536 private boolean isBundledApp() { 537 return (mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 538 } 539 isAppEnabled()540 private boolean isAppEnabled() { 541 return mAppEntry.info.enabled && !(mAppEntry.info.enabledSetting 542 == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED); 543 } 544 545 @Override processActivityResult(int requestCode, int resultCode, @Nullable Intent data)546 public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 547 if (requestCode == UNINSTALL_REQUEST_CODE 548 || requestCode == UNINSTALL_DEVICE_ADMIN_REQUEST_CODE) { 549 if (resultCode == RESULT_OK) { 550 getFragmentController().goBack(); 551 } else { 552 LOG.e("Uninstall failed with result " + resultCode); 553 } 554 } 555 } 556 ignoreActionBecauseItsDisabledByAdmin(List<String> restrictions)557 private boolean ignoreActionBecauseItsDisabledByAdmin(List<String> restrictions) { 558 if (mRestriction == null || !restrictions.contains(mRestriction)) return false; 559 560 LOG.d("Ignoring action because of " + mRestriction); 561 getFragmentController().showDialog(ActionDisabledByAdminDialogFragment.newInstance( 562 mRestriction, UserHandle.myUserId()), DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG); 563 return true; 564 } 565 } 566