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;
18 
19 import android.app.Notification;
20 import android.app.NotificationChannel;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.app.admin.IDevicePolicyManager;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.IPackageManager;
29 import android.content.pm.PackageInstaller;
30 import android.content.pm.PackageManager;
31 import android.content.pm.UserInfo;
32 import android.graphics.drawable.Icon;
33 import android.os.RemoteException;
34 import android.os.ServiceManager;
35 import android.os.UserHandle;
36 import android.os.UserManager;
37 import android.provider.Settings;
38 import android.support.annotation.NonNull;
39 import android.util.Log;
40 import android.widget.Toast;
41 
42 import java.util.List;
43 
44 /**
45  * Finish an uninstallation and show Toast on success or failure notification.
46  */
47 public class UninstallFinish extends BroadcastReceiver {
48     private static final String LOG_TAG = UninstallFinish.class.getSimpleName();
49 
50     private static final String UNINSTALL_FAILURE_CHANNEL = "uninstall failure";
51 
52     static final String EXTRA_UNINSTALL_ID = "com.android.packageinstaller.extra.UNINSTALL_ID";
53     static final String EXTRA_APP_LABEL = "com.android.packageinstaller.extra.APP_LABEL";
54 
55     @Override
onReceive(Context context, Intent intent)56     public void onReceive(Context context, Intent intent) {
57         int returnCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
58 
59         if (returnCode == PackageInstaller.STATUS_PENDING_USER_ACTION) {
60             context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT));
61             return;
62         }
63 
64         int uninstallId = intent.getIntExtra(EXTRA_UNINSTALL_ID, 0);
65         ApplicationInfo appInfo = intent.getParcelableExtra(
66                 PackageUtil.INTENT_ATTR_APPLICATION_INFO);
67         String appLabel = intent.getStringExtra(EXTRA_APP_LABEL);
68         boolean allUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, false);
69 
70         NotificationManager notificationManager =
71                 context.getSystemService(NotificationManager.class);
72         UserManager userManager = context.getSystemService(UserManager.class);
73 
74         NotificationChannel uninstallFailureChannel = new NotificationChannel(
75                 UNINSTALL_FAILURE_CHANNEL,
76                 context.getString(R.string.uninstall_failure_notification_channel),
77                 NotificationManager.IMPORTANCE_DEFAULT);
78         notificationManager.createNotificationChannel(uninstallFailureChannel);
79 
80         Notification.Builder uninstallFailedNotification = new Notification.Builder(context,
81                 UNINSTALL_FAILURE_CHANNEL);
82 
83         switch (returnCode) {
84             case PackageInstaller.STATUS_SUCCESS:
85                 notificationManager.cancel(uninstallId);
86 
87                 Toast.makeText(context, context.getString(R.string.uninstall_done_app, appLabel),
88                         Toast.LENGTH_LONG).show();
89                 return;
90             case PackageInstaller.STATUS_FAILURE_BLOCKED: {
91                 int legacyStatus = intent.getIntExtra(PackageInstaller.EXTRA_LEGACY_STATUS, 0);
92 
93                 switch (legacyStatus) {
94                     case PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER: {
95                         IDevicePolicyManager dpm = IDevicePolicyManager.Stub.asInterface(
96                                 ServiceManager.getService(Context.DEVICE_POLICY_SERVICE));
97                         // Find out if the package is an active admin for some non-current user.
98                         int myUserId = UserHandle.myUserId();
99                         UserInfo otherBlockingUser = null;
100                         for (UserInfo user : userManager.getUsers()) {
101                             // We only catch the case when the user in question is neither the
102                             // current user nor its profile.
103                             if (isProfileOfOrSame(userManager, myUserId, user.id)) {
104                                 continue;
105                             }
106 
107                             try {
108                                 if (dpm.packageHasActiveAdmins(appInfo.packageName, user.id)) {
109                                     otherBlockingUser = user;
110                                     break;
111                                 }
112                             } catch (RemoteException e) {
113                                 Log.e(LOG_TAG, "Failed to talk to package manager", e);
114                             }
115                         }
116                         if (otherBlockingUser == null) {
117                             Log.d(LOG_TAG, "Uninstall failed because " + appInfo.packageName
118                                     + " is a device admin");
119 
120                             addDeviceManagerButton(context, uninstallFailedNotification);
121                             setBigText(uninstallFailedNotification, context.getString(
122                                     R.string.uninstall_failed_device_policy_manager));
123                         } else {
124                             Log.d(LOG_TAG, "Uninstall failed because " + appInfo.packageName
125                                     + " is a device admin of user " + otherBlockingUser);
126 
127                             setBigText(uninstallFailedNotification, String.format(context.getString(
128                                     R.string.uninstall_failed_device_policy_manager_of_user),
129                                     otherBlockingUser.name));
130                         }
131                         break;
132                     }
133                     case PackageManager.DELETE_FAILED_OWNER_BLOCKED: {
134                         IPackageManager packageManager = IPackageManager.Stub.asInterface(
135                                 ServiceManager.getService("package"));
136 
137                         List<UserInfo> users = userManager.getUsers();
138                         int blockingUserId = UserHandle.USER_NULL;
139                         for (int i = 0; i < users.size(); ++i) {
140                             final UserInfo user = users.get(i);
141                             try {
142                                 if (packageManager.getBlockUninstallForUser(appInfo.packageName,
143                                         user.id)) {
144                                     blockingUserId = user.id;
145                                     break;
146                                 }
147                             } catch (RemoteException e) {
148                                 // Shouldn't happen.
149                                 Log.e(LOG_TAG, "Failed to talk to package manager", e);
150                             }
151                         }
152 
153                         int myUserId = UserHandle.myUserId();
154                         if (isProfileOfOrSame(userManager, myUserId, blockingUserId)) {
155                             addDeviceManagerButton(context, uninstallFailedNotification);
156                         } else {
157                             addManageUsersButton(context, uninstallFailedNotification);
158                         }
159 
160                         if (blockingUserId == UserHandle.USER_NULL) {
161                             Log.d(LOG_TAG,
162                                     "Uninstall failed for " + appInfo.packageName + " with code "
163                                             + returnCode + " no blocking user");
164                         } else if (blockingUserId == UserHandle.USER_SYSTEM) {
165                             setBigText(uninstallFailedNotification,
166                                     context.getString(R.string.uninstall_blocked_device_owner));
167                         } else {
168                             if (allUsers) {
169                                 setBigText(uninstallFailedNotification,
170                                         context.getString(
171                                                 R.string.uninstall_all_blocked_profile_owner));
172                             } else {
173                                 setBigText(uninstallFailedNotification, context.getString(
174                                         R.string.uninstall_blocked_profile_owner));
175                             }
176                         }
177                         break;
178                     }
179                     default:
180                         Log.d(LOG_TAG, "Uninstall blocked for " + appInfo.packageName
181                                 + " with legacy code " + legacyStatus);
182                 } break;
183             }
184             default:
185                 Log.d(LOG_TAG, "Uninstall failed for " + appInfo.packageName + " with code "
186                         + returnCode);
187                 break;
188         }
189 
190         uninstallFailedNotification.setContentTitle(
191                 context.getString(R.string.uninstall_failed_app, appLabel));
192         uninstallFailedNotification.setOngoing(false);
193         uninstallFailedNotification.setSmallIcon(R.drawable.ic_error);
194         notificationManager.notify(uninstallId, uninstallFailedNotification.build());
195     }
196 
197     /**
198      * Is a profile part of a user?
199      *
200      * @param userManager The user manager
201      * @param userId The id of the user
202      * @param profileId The id of the profile
203      *
204      * @return If the profile is part of the user or the profile parent of the user
205      */
isProfileOfOrSame(@onNull UserManager userManager, int userId, int profileId)206     private boolean isProfileOfOrSame(@NonNull UserManager userManager, int userId, int profileId) {
207         if (userId == profileId) {
208             return true;
209         }
210 
211         UserInfo parentUser = userManager.getProfileParent(profileId);
212         return parentUser != null && parentUser.id == userId;
213     }
214 
215     /**
216      * Set big text for the notification.
217      *
218      * @param builder The builder of the notification
219      * @param text The text to set.
220      */
setBigText(@onNull Notification.Builder builder, @NonNull CharSequence text)221     private void setBigText(@NonNull Notification.Builder builder,
222             @NonNull CharSequence text) {
223         builder.setStyle(new Notification.BigTextStyle().bigText(text));
224     }
225 
226     /**
227      * Add a button to the notification that links to the user management.
228      *
229      * @param context The context the notification is created in
230      * @param builder The builder of the notification
231      */
addManageUsersButton(@onNull Context context, @NonNull Notification.Builder builder)232     private void addManageUsersButton(@NonNull Context context,
233             @NonNull Notification.Builder builder) {
234         Intent intent = new Intent(Settings.ACTION_USER_SETTINGS);
235         intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK);
236 
237         builder.addAction((new Notification.Action.Builder(
238                 Icon.createWithResource(context, R.drawable.ic_settings_multiuser),
239                 context.getString(R.string.manage_users),
240                 PendingIntent.getActivity(context, 0, intent,
241                         PendingIntent.FLAG_UPDATE_CURRENT))).build());
242     }
243 
244     /**
245      * Add a button to the notification that links to the device policy management.
246      *
247      * @param context The context the notification is created in
248      * @param builder The builder of the notification
249      */
addDeviceManagerButton(@onNull Context context, @NonNull Notification.Builder builder)250     private void addDeviceManagerButton(@NonNull Context context,
251             @NonNull Notification.Builder builder) {
252         Intent intent = new Intent();
253         intent.setClassName("com.android.settings",
254                 "com.android.settings.Settings$DeviceAdminSettingsActivity");
255         intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK);
256 
257         builder.addAction((new Notification.Action.Builder(
258                 Icon.createWithResource(context, R.drawable.ic_lock),
259                 context.getString(R.string.manage_device_administrators),
260                 PendingIntent.getActivity(context, 0, intent,
261                         PendingIntent.FLAG_UPDATE_CURRENT))).build());
262     }
263 }
264