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