1 /* 2 * Copyright (C) 2016 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.packageinstaller.handheld; 18 19 import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; 20 import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; 21 import static android.text.format.Formatter.formatFileSize; 22 23 import android.app.AlertDialog; 24 import android.app.Dialog; 25 import android.app.DialogFragment; 26 import android.app.usage.StorageStats; 27 import android.app.usage.StorageStatsManager; 28 import android.content.DialogInterface; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.PackageInfo; 31 import android.content.pm.PackageManager; 32 import android.os.Bundle; 33 import android.os.Flags; 34 import android.os.Process; 35 import android.os.SystemProperties; 36 import android.os.UserHandle; 37 import android.os.UserManager; 38 import android.util.Log; 39 import android.view.LayoutInflater; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.widget.CheckBox; 43 import android.widget.TextView; 44 45 import androidx.annotation.NonNull; 46 import androidx.annotation.Nullable; 47 48 import com.android.packageinstaller.R; 49 import com.android.packageinstaller.UninstallerActivity; 50 51 import java.io.IOException; 52 import java.util.List; 53 54 public class UninstallAlertDialogFragment extends DialogFragment implements 55 DialogInterface.OnClickListener { 56 private static final String LOG_TAG = UninstallAlertDialogFragment.class.getSimpleName(); 57 58 private @Nullable CheckBox mKeepData; 59 private boolean mIsClonedApp; 60 61 /** 62 * Get number of bytes of the app data of the package. 63 * 64 * @param pkg The package that might have app data. 65 * @param user The user the package belongs to 66 * 67 * @return The number of bytes. 68 */ getAppDataSizeForUser(@onNull String pkg, @NonNull UserHandle user)69 private long getAppDataSizeForUser(@NonNull String pkg, @NonNull UserHandle user) { 70 StorageStatsManager storageStatsManager = 71 getContext().getSystemService(StorageStatsManager.class); 72 try { 73 StorageStats stats = storageStatsManager.queryStatsForPackage( 74 getContext().getPackageManager().getApplicationInfo(pkg, 0).storageUuid, 75 pkg, user); 76 return stats.getDataBytes(); 77 } catch (PackageManager.NameNotFoundException | IOException | SecurityException e) { 78 Log.e(LOG_TAG, "Cannot determine amount of app data for " + pkg, e); 79 } 80 81 return 0; 82 } 83 84 /** 85 * Get number of bytes of the app data of the package. 86 * 87 * @param pkg The package that might have app data. 88 * @param user The user the package belongs to or {@code null} if files of all users should be 89 * counted. 90 * 91 * @return The number of bytes. 92 */ getAppDataSize(@onNull String pkg, @Nullable UserHandle user)93 private long getAppDataSize(@NonNull String pkg, @Nullable UserHandle user) { 94 UserManager userManager = getContext().getSystemService(UserManager.class); 95 96 long appDataSize = 0; 97 98 if (user == null) { 99 List<UserHandle> userHandles = userManager.getUserHandles(true); 100 101 int numUsers = userHandles.size(); 102 for (int i = 0; i < numUsers; i++) { 103 appDataSize += getAppDataSizeForUser(pkg, userHandles.get(i)); 104 } 105 } else { 106 appDataSize = getAppDataSizeForUser(pkg, user); 107 } 108 109 return appDataSize; 110 } 111 112 @Override onCreateDialog(Bundle savedInstanceState)113 public Dialog onCreateDialog(Bundle savedInstanceState) { 114 final PackageManager pm = getActivity().getPackageManager(); 115 final UninstallerActivity.DialogInfo dialogInfo = 116 ((UninstallerActivity) getActivity()).getDialogInfo(); 117 final CharSequence appLabel = dialogInfo.appInfo.loadSafeLabel(pm); 118 AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); 119 StringBuilder messageBuilder = new StringBuilder(); 120 121 // If the Activity label differs from the App label, then make sure the user 122 // knows the Activity belongs to the App being uninstalled. 123 if (dialogInfo.activityInfo != null) { 124 final CharSequence activityLabel = dialogInfo.activityInfo.loadSafeLabel(pm); 125 if (!activityLabel.equals(appLabel)) { 126 messageBuilder.append( 127 getString(R.string.uninstall_activity_text, activityLabel)); 128 messageBuilder.append(" ").append(appLabel).append(".\n\n"); 129 } 130 } 131 132 final boolean isUpdate = 133 ((dialogInfo.appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); 134 final boolean isArchive = 135 isArchivingEnabled() && ( 136 (dialogInfo.deleteFlags & PackageManager.DELETE_ARCHIVE) != 0); 137 final UserHandle myUserHandle = Process.myUserHandle(); 138 UserManager userManager = getContext().getSystemService(UserManager.class); 139 if (isUpdate) { 140 if (isSingleUser(userManager)) { 141 messageBuilder.append(getString(R.string.uninstall_update_text)); 142 } else { 143 messageBuilder.append(getString(R.string.uninstall_update_text_multiuser)); 144 } 145 } else { 146 if (dialogInfo.allUsers && !isSingleUser(userManager)) { 147 messageBuilder.append( 148 isArchive ? getString(R.string.archive_application_text_all_users) 149 : getString(R.string.uninstall_application_text_all_users)); 150 } else if (!dialogInfo.user.equals(myUserHandle)) { 151 int userId = dialogInfo.user.getIdentifier(); 152 UserManager customUserManager = getContext() 153 .createContextAsUser(UserHandle.of(userId), 0) 154 .getSystemService(UserManager.class); 155 String userName = customUserManager.getUserName(); 156 157 if (customUserManager.isUserOfType(USER_TYPE_PROFILE_MANAGED) 158 && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) { 159 messageBuilder.append(isArchive 160 ? getString(R.string.archive_application_text_current_user_work_profile) 161 : getString( 162 R.string.uninstall_application_text_current_user_work_profile)); 163 } else if (customUserManager.isUserOfType(USER_TYPE_PROFILE_CLONE) 164 && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) { 165 mIsClonedApp = true; 166 messageBuilder.append(getString( 167 R.string.uninstall_application_text_current_user_clone_profile)); 168 } else if (Flags.allowPrivateProfile() 169 && android.multiuser.Flags.enablePrivateSpaceFeatures() 170 && customUserManager.isPrivateProfile() 171 && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) { 172 messageBuilder.append( 173 isArchive ? getString( 174 R.string.archive_application_text_current_user_private_profile) 175 : getString( 176 R.string.uninstall_application_text_current_user_private_profile)); 177 } else if (isArchive) { 178 messageBuilder.append( 179 getString(R.string.archive_application_text_user, userName)); 180 } else { 181 messageBuilder.append( 182 getString(R.string.uninstall_application_text_user, userName)); 183 } 184 } else if (isCloneProfile(myUserHandle)) { 185 mIsClonedApp = true; 186 messageBuilder.append(getString( 187 R.string.uninstall_application_text_current_user_clone_profile)); 188 } else if (Process.myUserHandle().equals(UserHandle.SYSTEM) 189 && hasClonedInstance(dialogInfo.appInfo.packageName)) { 190 messageBuilder.append(getString( 191 R.string.uninstall_application_text_with_clone_instance, 192 appLabel)); 193 } else if (isArchive) { 194 messageBuilder.append(getString(R.string.archive_application_text)); 195 } else { 196 messageBuilder.append(getString(R.string.uninstall_application_text)); 197 } 198 } 199 200 if (mIsClonedApp) { 201 dialogBuilder.setTitle(getString(R.string.cloned_app_label, appLabel)); 202 } else if (isArchive) { 203 dialogBuilder.setTitle(getString(R.string.archiving_app_label, appLabel)); 204 } else { 205 dialogBuilder.setTitle(appLabel); 206 } 207 dialogBuilder.setPositiveButton(isArchive ? R.string.archive : android.R.string.ok, 208 this); 209 dialogBuilder.setNegativeButton(android.R.string.cancel, this); 210 211 String pkg = dialogInfo.appInfo.packageName; 212 213 boolean suggestToKeepAppData; 214 try { 215 PackageInfo pkgInfo = pm.getPackageInfo(pkg, 216 PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ARCHIVED_PACKAGES)); 217 218 suggestToKeepAppData = pkgInfo.applicationInfo.hasFragileUserData() && !isArchive; 219 } catch (PackageManager.NameNotFoundException e) { 220 Log.e(LOG_TAG, "Cannot check hasFragileUserData for " + pkg, e); 221 suggestToKeepAppData = false; 222 } 223 224 long appDataSize = 0; 225 if (suggestToKeepAppData) { 226 appDataSize = getAppDataSize(pkg, dialogInfo.allUsers ? null : dialogInfo.user); 227 } 228 229 if (appDataSize == 0) { 230 dialogBuilder.setMessage(messageBuilder.toString()); 231 } else { 232 LayoutInflater inflater = getContext().getSystemService(LayoutInflater.class); 233 ViewGroup content = (ViewGroup) inflater.inflate(R.layout.uninstall_content_view, null); 234 235 ((TextView) content.requireViewById(R.id.message)).setText(messageBuilder.toString()); 236 mKeepData = content.requireViewById(R.id.keepData); 237 mKeepData.setVisibility(View.VISIBLE); 238 mKeepData.setText(getString(R.string.uninstall_keep_data, 239 formatFileSize(getContext(), appDataSize))); 240 241 dialogBuilder.setView(content); 242 } 243 244 return dialogBuilder.create(); 245 } 246 isArchivingEnabled()247 private static boolean isArchivingEnabled() { 248 return android.content.pm.Flags.archiving(); 249 } 250 isCloneProfile(UserHandle userHandle)251 private boolean isCloneProfile(UserHandle userHandle) { 252 UserManager customUserManager = getContext() 253 .createContextAsUser(UserHandle.of(userHandle.getIdentifier()), 0) 254 .getSystemService(UserManager.class); 255 if (customUserManager.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE)) { 256 return true; 257 } 258 return false; 259 } 260 hasClonedInstance(String packageName)261 private boolean hasClonedInstance(String packageName) { 262 // Check if clone user is present on the device. 263 UserHandle cloneUser = null; 264 UserManager userManager = getContext().getSystemService(UserManager.class); 265 List<UserHandle> profiles = userManager.getUserProfiles(); 266 for (UserHandle userHandle : profiles) { 267 if (!userHandle.equals(UserHandle.SYSTEM) && isCloneProfile(userHandle)) { 268 cloneUser = userHandle; 269 break; 270 } 271 } 272 273 // Check if another instance of given package exists in clone user profile. 274 if (cloneUser != null) { 275 try { 276 if (getContext().getPackageManager().getPackageUidAsUser(packageName, 277 PackageManager.PackageInfoFlags.of(0), cloneUser.getIdentifier()) > 0) { 278 return true; 279 } 280 } catch (PackageManager.NameNotFoundException e) { 281 return false; 282 } 283 } 284 return false; 285 } 286 287 @Override onClick(DialogInterface dialog, int which)288 public void onClick(DialogInterface dialog, int which) { 289 if (which == Dialog.BUTTON_POSITIVE) { 290 ((UninstallerActivity) getActivity()).startUninstallProgress( 291 mKeepData != null && mKeepData.isChecked(), mIsClonedApp); 292 } else { 293 ((UninstallerActivity) getActivity()).dispatchAborted(); 294 } 295 } 296 297 @Override onDismiss(DialogInterface dialog)298 public void onDismiss(DialogInterface dialog) { 299 super.onDismiss(dialog); 300 if (isAdded()) { 301 getActivity().finish(); 302 } 303 } 304 305 /** 306 * Returns whether there is only one "full" user on this device. 307 * 308 * <p><b>Note:</b> on devices that use {@link android.os.UserManager#isHeadlessSystemUserMode() 309 * headless system user mode}, the system user is not "full", so it's not be considered in the 310 * calculation. 311 */ isSingleUser(UserManager userManager)312 private boolean isSingleUser(UserManager userManager) { 313 final int userCount = userManager.getUserCount(); 314 return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2); 315 } 316 } 317