summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilip P. Moltmann <moltmann@google.com>2016-10-25 12:05:58 -0700
committerPhilip P. Moltmann <moltmann@google.com>2016-11-02 09:42:29 -0700
commitde66f874cb82a66b51149e94f8f4aaba52cb7121 (patch)
tree96af43feef58675e6030f46a923609e0602d7ffa
parent4e91e0324f0742203909c82c13a17641711a3acb (diff)
downloadandroid_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.xml3
-rw-r--r--res/drawable/ic_error.xml26
-rw-r--r--res/drawable/ic_remove.xml26
-rw-r--r--res/drawable/ic_settings_multiuser.xml25
-rw-r--r--res/values/strings.xml3
-rw-r--r--src/com/android/packageinstaller/UninstallFinish.java243
-rwxr-xr-xsrc/com/android/packageinstaller/UninstallerActivity.java53
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() {