/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.packageinstaller; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.graphics.drawable.Icon; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; import java.util.List; /** * Finish an uninstallation and show Toast on success or failure notification. */ public class UninstallFinish extends BroadcastReceiver { private static final String LOG_TAG = UninstallFinish.class.getSimpleName(); private static final String UNINSTALL_FAILURE_CHANNEL = "uninstall failure"; static final String EXTRA_UNINSTALL_ID = "com.android.packageinstaller.extra.UNINSTALL_ID"; static final String EXTRA_APP_LABEL = "com.android.packageinstaller.extra.APP_LABEL"; static final String EXTRA_IS_CLONE_APP = "com.android.packageinstaller.extra.IS_CLONE_APP"; @Override public void onReceive(Context context, Intent intent) { int returnCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0); Log.i(LOG_TAG, "Uninstall finished extras=" + intent.getExtras()); if (returnCode == PackageInstaller.STATUS_PENDING_USER_ACTION) { context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT)); return; } int uninstallId = intent.getIntExtra(EXTRA_UNINSTALL_ID, 0); ApplicationInfo appInfo = intent.getParcelableExtra( PackageUtil.INTENT_ATTR_APPLICATION_INFO); String appLabel = intent.getStringExtra(EXTRA_APP_LABEL); boolean allUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, false); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); UserManager userManager = context.getSystemService(UserManager.class); NotificationChannel uninstallFailureChannel = new NotificationChannel( UNINSTALL_FAILURE_CHANNEL, context.getString(R.string.uninstall_failure_notification_channel), NotificationManager.IMPORTANCE_DEFAULT); notificationManager.createNotificationChannel(uninstallFailureChannel); Notification.Builder uninstallFailedNotification = new Notification.Builder(context, UNINSTALL_FAILURE_CHANNEL); switch (returnCode) { case PackageInstaller.STATUS_SUCCESS: notificationManager.cancel(uninstallId); boolean isCloneApp = intent.getBooleanExtra(EXTRA_IS_CLONE_APP, false); Toast.makeText(context, isCloneApp ? context.getString(R.string.uninstall_done_clone_app, appLabel) : context.getString(R.string.uninstall_done_app, appLabel), Toast.LENGTH_LONG).show(); return; case PackageInstaller.STATUS_FAILURE_BLOCKED: { int legacyStatus = intent.getIntExtra(PackageInstaller.EXTRA_LEGACY_STATUS, 0); switch (legacyStatus) { case PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER: { // Find out if the package is an active admin for some non-current user. UserHandle myUserHandle = Process.myUserHandle(); UserHandle otherBlockingUserHandle = null; for (UserHandle otherUserHandle : userManager.getUserHandles(true)) { // We only catch the case when the user in question is neither the // current user nor its profile. if (isProfileOfOrSame(userManager, myUserHandle, otherUserHandle)) { continue; } DevicePolicyManager dpm = context.createContextAsUser(otherUserHandle, 0) .getSystemService(DevicePolicyManager.class); if (dpm.packageHasActiveAdmins(appInfo.packageName)) { otherBlockingUserHandle = otherUserHandle; break; } } if (otherBlockingUserHandle == null) { Log.d(LOG_TAG, "Uninstall failed because " + appInfo.packageName + " is a device admin"); addDeviceManagerButton(context, uninstallFailedNotification); setBigText(uninstallFailedNotification, context.getString( R.string.uninstall_failed_device_policy_manager)); } else { Log.d(LOG_TAG, "Uninstall failed because " + appInfo.packageName + " is a device admin of user " + otherBlockingUserHandle); String userName = context.createContextAsUser(otherBlockingUserHandle, 0) .getSystemService(UserManager.class).getUserName(); setBigText(uninstallFailedNotification, String.format(context.getString( R.string.uninstall_failed_device_policy_manager_of_user), userName)); } break; } case PackageManager.DELETE_FAILED_OWNER_BLOCKED: { PackageManager packageManager = context.getPackageManager(); List userHandles = userManager.getUserHandles(true); UserHandle otherBlockingUserHandle = null; for (int i = 0; i < userHandles.size(); ++i) { final UserHandle handle = userHandles.get(i); if (packageManager.canUserUninstall(appInfo.packageName, handle)) { otherBlockingUserHandle = handle; break; } } UserHandle myUserHandle = Process.myUserHandle(); if (isProfileOfOrSame(userManager, myUserHandle, otherBlockingUserHandle)) { addDeviceManagerButton(context, uninstallFailedNotification); } else { addManageUsersButton(context, uninstallFailedNotification); } if (otherBlockingUserHandle == null) { Log.d(LOG_TAG, "Uninstall failed for " + appInfo.packageName + " with code " + returnCode + " no blocking user"); } else if (otherBlockingUserHandle == UserHandle.SYSTEM) { setBigText(uninstallFailedNotification, context.getString(R.string.uninstall_blocked_device_owner)); } else { if (allUsers) { setBigText(uninstallFailedNotification, context.getString( R.string.uninstall_all_blocked_profile_owner)); } else { setBigText(uninstallFailedNotification, context.getString( R.string.uninstall_blocked_profile_owner)); } } break; } default: Log.d(LOG_TAG, "Uninstall blocked for " + appInfo.packageName + " with legacy code " + legacyStatus); } break; } default: Log.d(LOG_TAG, "Uninstall failed for " + appInfo.packageName + " with code " + returnCode); break; } uninstallFailedNotification.setContentTitle( context.getString(R.string.uninstall_failed_app, appLabel)); uninstallFailedNotification.setOngoing(false); uninstallFailedNotification.setSmallIcon(R.drawable.ic_error); notificationManager.notify(uninstallId, uninstallFailedNotification.build()); } /** * Is a profile part of a user? * * @param userManager The user manager * @param userHandle The handle of the user * @param profileHandle The handle of the profile * * @return If the profile is part of the user or the profile parent of the user */ private boolean isProfileOfOrSame(UserManager userManager, UserHandle userHandle, UserHandle profileHandle) { if (userHandle.equals(profileHandle)) { return true; } return userManager.getProfileParent(profileHandle) != null && userManager.getProfileParent(profileHandle).equals(userHandle); } /** * Set big text for the notification. * * @param builder The builder of the notification * @param text The text to set. */ private void setBigText(@NonNull Notification.Builder builder, @NonNull CharSequence text) { builder.setStyle(new Notification.BigTextStyle().bigText(text)); } /** * Add a button to the notification that links to the user management. * * @param context The context the notification is created in * @param builder The builder of the notification */ private void addManageUsersButton(@NonNull Context context, @NonNull Notification.Builder builder) { Intent intent = new Intent(Settings.ACTION_USER_SETTINGS); intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK); builder.addAction((new Notification.Action.Builder( Icon.createWithResource(context, R.drawable.ic_settings_multiuser), context.getString(R.string.manage_users), PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))).build()); } /** * Add a button to the notification that links to the device policy management. * * @param context The context the notification is created in * @param builder The builder of the notification */ private void addDeviceManagerButton(@NonNull Context context, @NonNull Notification.Builder builder) { Intent intent = new Intent(); intent.setClassName("com.android.settings", "com.android.settings.Settings$DeviceAdminSettingsActivity"); intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK); builder.addAction((new Notification.Action.Builder( Icon.createWithResource(context, R.drawable.ic_lock), context.getString(R.string.manage_device_administrators), PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))).build()); } }