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