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