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