diff options
author | Philip P. Moltmann <moltmann@google.com> | 2016-10-25 12:05:58 -0700 |
---|---|---|
committer | Philip P. Moltmann <moltmann@google.com> | 2016-11-02 09:42:29 -0700 |
commit | de66f874cb82a66b51149e94f8f4aaba52cb7121 (patch) | |
tree | 96af43feef58675e6030f46a923609e0602d7ffa | |
parent | 4e91e0324f0742203909c82c13a17641711a3acb (diff) | |
download | android_packages_apps_PackageInstaller-de66f874cb82a66b51149e94f8f4aaba52cb7121.tar.gz android_packages_apps_PackageInstaller-de66f874cb82a66b51149e94f8f4aaba52cb7121.tar.bz2 android_packages_apps_PackageInstaller-de66f874cb82a66b51149e94f8f4aaba52cb7121.zip |
No activity while uninstalling unless needed
In many cases uninstalling does not require any acticity. In this case,
just show a notification instead of a problematic translucent activity.
Note: In a future change this will also allow uninstallation for all
users. This might need API changes though.
Test: Simulated all error and success scenarios
Change-Id: Iaa655fecc981b7274b995608f6e9b428f84a8d7d
-rw-r--r-- | AndroidManifest.xml | 3 | ||||
-rw-r--r-- | res/drawable/ic_error.xml | 26 | ||||
-rw-r--r-- | res/drawable/ic_remove.xml | 26 | ||||
-rw-r--r-- | res/drawable/ic_settings_multiuser.xml | 25 | ||||
-rw-r--r-- | res/values/strings.xml | 3 | ||||
-rw-r--r-- | src/com/android/packageinstaller/UninstallFinish.java | 243 | ||||
-rwxr-xr-x | src/com/android/packageinstaller/UninstallerActivity.java | 53 |
7 files changed, 369 insertions, 10 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index dd550af9..6b4d8fd1 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -83,6 +83,9 @@ </intent-filter> </activity> + <receiver android:name=".UninstallFinish" + android:exported="false" /> + <activity android:name=".UninstallAppProgress" android:configChanges="mnc|mnc|touchscreen|navigation|screenLayout|screenSize|smallestScreenSize|orientation|locale|keyboard|keyboardHidden|fontScale|uiMode|layoutDirection|density" android:exported="false" /> diff --git a/res/drawable/ic_error.xml b/res/drawable/ic_error.xml new file mode 100644 index 00000000..28612a1c --- /dev/null +++ b/res/drawable/ic_error.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" + android:fillColor="#000000"/> +</vector> diff --git a/res/drawable/ic_remove.xml b/res/drawable/ic_remove.xml new file mode 100644 index 00000000..dd46eda6 --- /dev/null +++ b/res/drawable/ic_remove.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13L7,13v-2h10v2z" + android:fillColor="#000000"/> +</vector> diff --git a/res/drawable/ic_settings_multiuser.xml b/res/drawable/ic_settings_multiuser.xml new file mode 100644 index 00000000..b24a5d43 --- /dev/null +++ b/res/drawable/ic_settings_multiuser.xml @@ -0,0 +1,25 @@ +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?android:attr/colorAccent"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M12.0,12.0c2.21,0.0 4.0,-1.79 4.0,-4.0s-1.79,-4.0 -4.0,-4.0 -4.0,1.79 -4.0,4.0 1.79,4.0 4.0,4.0zm0.0,2.0c-2.67,0.0 -8.0,1.34 -8.0,4.0l0.0,2.0l16.0,0.0l0.0,-2.0c0.0,-2.66 -5.33,-4.0 -8.0,-4.0z"/> +</vector> diff --git a/res/values/strings.xml b/res/values/strings.xml index f5aace75..b1c7c643 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -108,8 +108,11 @@ <string name="uninstall_update_text">Replace this app with the factory version? All data will be removed.</string> <string name="uninstall_update_text_multiuser">Replace this app with the factory version? All data will be removed. This affects all users of this device, including those with work profiles.</string> <string name="uninstalling">Uninstalling\u2026</string> + <string name="uninstalling_app">Uninstalling <xliff:g id="package_label">%1$s</xliff:g>\u2026</string> <string name="uninstall_done">Uninstall finished.</string> + <string name="uninstall_done_app">Uninstalled <xliff:g id="package_label">%1$s</xliff:g>.</string> <string name="uninstall_failed">Uninstall unsuccessful.</string> + <string name="uninstall_failed_app">Uninstalling <xliff:g id="package_label">%1$s</xliff:g> unsuccessful.</string> <!-- String presented to the user when uninstalling a package failed because the target package is a current device administrator [CHAR LIMIT=80] --> <string name="uninstall_failed_device_policy_manager">Can\'t uninstall because this package is an diff --git a/src/com/android/packageinstaller/UninstallFinish.java b/src/com/android/packageinstaller/UninstallFinish.java new file mode 100644 index 00000000..06e5a7c0 --- /dev/null +++ b/src/com/android/packageinstaller/UninstallFinish.java @@ -0,0 +1,243 @@ +/* + * 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.annotation.NonNull; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.admin.IDevicePolicyManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.graphics.drawable.Icon; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.util.Log; +import android.widget.Toast; + +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(); + + static final String EXTRA_UNINSTALL_ID = "com.android.packageinstaller.extra.UNINSTALL_ID"; + static final String EXTRA_APP_INFO = "com.android.packageinstaller.extra.APP_INFO"; + static final String EXTRA_APP_LABEL = "com.android.packageinstaller.extra.APP_LABEL"; + + @Override + public void onReceive(Context context, Intent intent) { + int returnCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0); + + 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(EXTRA_APP_INFO); + String appLabel = intent.getStringExtra(EXTRA_APP_LABEL); + + NotificationManager notificationManager = + context.getSystemService(NotificationManager.class); + UserManager userManager = context.getSystemService(UserManager.class); + + Notification.Builder uninstallFailedNotification = new Notification.Builder(context); + + switch (returnCode) { + case PackageInstaller.STATUS_SUCCESS: + notificationManager.cancel(uninstallId); + + Toast.makeText(context, 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: { + IDevicePolicyManager dpm = IDevicePolicyManager.Stub.asInterface( + ServiceManager.getService(Context.DEVICE_POLICY_SERVICE)); + // Find out if the package is an active admin for some non-current user. + int myUserId = UserHandle.myUserId(); + UserInfo otherBlockingUser = null; + for (UserInfo user : userManager.getUsers()) { + // We only catch the case when the user in question is neither the + // current user nor its profile. + if (isProfileOfOrSame(userManager, myUserId, user.id)) { + continue; + } + + try { + if (dpm.packageHasActiveAdmins(appInfo.packageName, user.id)) { + otherBlockingUser = user; + break; + } + } catch (RemoteException e) { + Log.e(LOG_TAG, "Failed to talk to package manager", e); + } + } + if (otherBlockingUser == 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 " + otherBlockingUser); + + setBigText(uninstallFailedNotification, String.format(context.getString( + R.string.uninstall_failed_device_policy_manager_of_user), + otherBlockingUser.name)); + } + break; + } + case PackageManager.DELETE_FAILED_OWNER_BLOCKED: { + IPackageManager packageManager = IPackageManager.Stub.asInterface( + ServiceManager.getService("package")); + + List<UserInfo> users = userManager.getUsers(); + int blockingUserId = UserHandle.USER_NULL; + for (int i = 0; i < users.size(); ++i) { + final UserInfo user = users.get(i); + try { + if (packageManager.getBlockUninstallForUser(appInfo.packageName, + user.id)) { + blockingUserId = user.id; + break; + } + } catch (RemoteException e) { + // Shouldn't happen. + Log.e(LOG_TAG, "Failed to talk to package manager", e); + } + } + + int myUserId = UserHandle.myUserId(); + if (isProfileOfOrSame(userManager, myUserId, blockingUserId)) { + addDeviceManagerButton(context, uninstallFailedNotification); + } else { + addManageUsersButton(context, uninstallFailedNotification); + } + + if (blockingUserId == UserHandle.USER_NULL) { + Log.d(LOG_TAG, + "Uninstall failed for " + appInfo.packageName + " with code " + + returnCode + " no blocking user"); + } else if (blockingUserId == UserHandle.USER_SYSTEM) { + 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 userId The id of the user + * @param profileId The id of the profile + * + * @return If the profile is part of the user or the profile parent of the user + */ + private boolean isProfileOfOrSame(@NonNull UserManager userManager, int userId, int profileId) { + if (userId == profileId) { + return true; + } + + UserInfo parentUser = userManager.getProfileParent(profileId); + return parentUser != null && parentUser.id == userId; + } + + /** + * 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))).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))).build()); + } +} diff --git a/src/com/android/packageinstaller/UninstallerActivity.java b/src/com/android/packageinstaller/UninstallerActivity.java index 533902cb..d7aecc3f 100755 --- a/src/com/android/packageinstaller/UninstallerActivity.java +++ b/src/com/android/packageinstaller/UninstallerActivity.java @@ -20,6 +20,9 @@ import android.app.Activity; import android.app.DialogFragment; import android.app.Fragment; import android.app.FragmentTransaction; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -42,6 +45,8 @@ import com.android.packageinstaller.handheld.UninstallAlertDialogFragment; import com.android.packageinstaller.television.AppNotFoundFragment; import com.android.packageinstaller.television.UninstallAlertFragment; +import java.util.Random; + /* * This activity presents UI to uninstall an application. Usually launched with intent * Intent.ACTION_UNINSTALL_PKG_COMMAND and attribute @@ -164,17 +169,45 @@ public class UninstallerActivity extends Activity { } public void startUninstallProgress() { - Intent newIntent = new Intent(Intent.ACTION_VIEW); - newIntent.putExtra(Intent.EXTRA_USER, mDialogInfo.user); - newIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, mDialogInfo.allUsers); - newIntent.putExtra(PackageInstaller.EXTRA_CALLBACK, mDialogInfo.callback); - newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mDialogInfo.appInfo); - if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { - newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); - newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + boolean returnResult = getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false); + if (isTv() || returnResult || mDialogInfo.allUsers || mDialogInfo.callback != null) { + Intent newIntent = new Intent(Intent.ACTION_VIEW); + newIntent.putExtra(Intent.EXTRA_USER, mDialogInfo.user); + newIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, mDialogInfo.allUsers); + newIntent.putExtra(PackageInstaller.EXTRA_CALLBACK, mDialogInfo.callback); + newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mDialogInfo.appInfo); + + if (returnResult) { + newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); + newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + } + + newIntent.setClass(this, UninstallAppProgress.class); + startActivity(newIntent); + } else { + int uninstallId = (new Random()).nextInt(); + CharSequence label = mDialogInfo.appInfo.loadLabel(getPackageManager()); + + Intent broadcastIntent = new Intent(this, UninstallFinish.class); + broadcastIntent.putExtra(UninstallFinish.EXTRA_APP_INFO, mDialogInfo.appInfo); + broadcastIntent.putExtra(UninstallFinish.EXTRA_APP_LABEL, label); + broadcastIntent.putExtra(UninstallFinish.EXTRA_UNINSTALL_ID, uninstallId); + + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, uninstallId, + broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + Notification uninstallingNotification = (new Notification.Builder(this)) + .setSmallIcon(R.drawable.ic_remove).setProgress(0, 1, true) + .setContentTitle(getString(R.string.uninstalling_app, label)).setOngoing(true) + .setPriority(Notification.PRIORITY_MIN) + .build(); + + getSystemService(NotificationManager.class).notify(uninstallId, + uninstallingNotification); + + getPackageManager().getPackageInstaller().uninstall(mDialogInfo.appInfo.packageName, + pendingIntent.getIntentSender()); } - newIntent.setClass(this, UninstallAppProgress.class); - startActivity(newIntent); } public void dispatchAborted() { |