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