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