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