diff --git a/api/current.txt b/api/current.txt
index 3b9ac8a7f0beb..78cc34e91abe6 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -703,6 +703,7 @@ package android {
field public static final int hapticFeedbackEnabled = 16843358; // 0x101025e
field public static final int hardwareAccelerated = 16843475; // 0x10102d3
field public static final int hasCode = 16842764; // 0x101000c
+ field public static final int hasFragileUserData = 16844192; // 0x10105a0
field public static final deprecated int headerAmPmTextAppearance = 16843936; // 0x10104a0
field public static final int headerBackground = 16843055; // 0x101012f
field public static final deprecated int headerDayOfMonthTextAppearance = 16843927; // 0x1010497
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 07d6e47857599..c361ac12667ed 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -631,6 +631,13 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
*/
public static final int PRIVATE_FLAG_PROFILEABLE_BY_SHELL = 1 << 23;
+ /**
+ * Indicates whether this package requires access to non-SDK APIs.
+ * Only system apps and tests are allowed to use this property.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_HAS_FRAGILE_USER_DATA = 1 << 24;
+
/** @hide */
@IntDef(flag = true, prefix = { "PRIVATE_FLAG_" }, value = {
PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE,
@@ -655,6 +662,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
PRIVATE_FLAG_STATIC_SHARED_LIBRARY,
PRIVATE_FLAG_VENDOR,
PRIVATE_FLAG_VIRTUAL_PRELOAD,
+ PRIVATE_FLAG_HAS_FRAGILE_USER_DATA,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ApplicationInfoPrivateFlags {}
@@ -1730,6 +1738,17 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
return (privateFlags & PRIVATE_FLAG_USES_NON_SDK_API) != 0;
}
+ /**
+ * Whether an app needs to keep the app data on uninstall.
+ *
+ * @return {@code true} if the app indicates that it needs to keep the app data
+ *
+ * @hide
+ */
+ public boolean hasFragileUserData() {
+ return (privateFlags & PRIVATE_FLAG_HAS_FRAGILE_USER_DATA) != 0;
+ }
+
private boolean isAllowedToUseHiddenApis() {
if (isSignedWithPlatformKey()) {
return true;
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index d0de9a1d2a762..61a74ded02d07 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -3710,6 +3710,12 @@ public class PackageParser {
ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_USES_NON_SDK_API;
}
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestApplication_hasFragileUserData,
+ false)) {
+ ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_HAS_FRAGILE_USER_DATA;
+ }
+
if (outError[0] == null) {
CharSequence pname;
if (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.FROYO) {
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index ab4bd0534025d..35263a3fa8919 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1603,6 +1603,9 @@
+
+
+
Also remove %1$s of associated media files.
+
+ Keep %1$s of app data.
Running uninstalls
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
index d13bb65604af4..63d8c5a82519c 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
@@ -52,6 +52,7 @@ public class UninstallUninstalling extends Activity implements
static final String EXTRA_APP_LABEL = "com.android.packageinstaller.extra.APP_LABEL";
static final String EXTRA_CLEAR_CONTRIBUTED_FILES =
"com.android.packageinstaller.extra.CLEAR_CONTRIBUTED_FILES";
+ static final String EXTRA_KEEP_DATA = "com.android.packageinstaller.extra.KEEP_DATA";
private int mUninstallId;
private ApplicationInfo mAppInfo;
@@ -76,6 +77,7 @@ public class UninstallUninstalling extends Activity implements
false);
boolean clearContributedFiles = getIntent().getBooleanExtra(
EXTRA_CLEAR_CONTRIBUTED_FILES, false);
+ boolean keepData = getIntent().getBooleanExtra(EXTRA_KEEP_DATA, false);
UserHandle user = getIntent().getParcelableExtra(Intent.EXTRA_USER);
// Show dialog, which is the whole UI
@@ -101,6 +103,7 @@ public class UninstallUninstalling extends Activity implements
int flags = allUsers ? PackageManager.DELETE_ALL_USERS : 0;
flags |= clearContributedFiles ? PackageManager.DELETE_CONTRIBUTED_MEDIA : 0;
+ flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
try {
ActivityThread.getPackageManager().getPackageInstaller().uninstall(
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
index 0fa8c9a688c77..54194491d140b 100755
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
@@ -285,7 +285,7 @@ public class UninstallerActivity extends Activity {
fragment.show(ft, "dialog");
}
- public void startUninstallProgress(boolean clearContributedFiles) {
+ public void startUninstallProgress(boolean clearContributedFiles, boolean keepData) {
boolean returnResult = getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
CharSequence label = mDialogInfo.appInfo.loadSafeLabel(getPackageManager());
@@ -312,6 +312,7 @@ public class UninstallerActivity extends Activity {
newIntent.putExtra(UninstallUninstalling.EXTRA_APP_LABEL, label);
newIntent.putExtra(UninstallUninstalling.EXTRA_CLEAR_CONTRIBUTED_FILES,
clearContributedFiles);
+ newIntent.putExtra(UninstallUninstalling.EXTRA_KEEP_DATA, keepData);
newIntent.putExtra(PackageInstaller.EXTRA_CALLBACK, mDialogInfo.callback);
if (returnResult) {
@@ -362,6 +363,7 @@ public class UninstallerActivity extends Activity {
int flags = mDialogInfo.allUsers ? PackageManager.DELETE_ALL_USERS : 0;
flags |= clearContributedFiles ? PackageManager.DELETE_CONTRIBUTED_MEDIA : 0;
+ flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
ActivityThread.getPackageManager().getPackageInstaller().uninstall(
new VersionedPackage(mDialogInfo.appInfo.packageName,
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
index e4e12759211d6..499da758739ef 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
@@ -16,21 +16,30 @@
package com.android.packageinstaller.handheld;
+import static android.os.storage.StorageManager.convert;
import static android.text.format.Formatter.formatFileSize;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
+import android.app.usage.StorageStats;
+import android.app.usage.StorageStatsManager;
import android.content.DialogInterface;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
import android.provider.MediaStore;
+import android.util.Log;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
@@ -43,25 +52,120 @@ import java.util.List;
public class UninstallAlertDialogFragment extends DialogFragment implements
DialogInterface.OnClickListener {
+ private static final String LOG_TAG = UninstallAlertDialogFragment.class.getSimpleName();
- private CheckBox mClearContributedFiles;
+ private @Nullable CheckBox mClearContributedFiles;
+ private @Nullable CheckBox mKeepData;
/**
- * Get number of bytes of the combined files contributed by the package.
+ * Get number of bytes of the files contributed by the package.
*
- * @param pkg The package that might have contibuted files.
+ * @param pkg The package that might have contributed files.
* @param user The user the package belongs to.
*
* @return The number of bytes.
*/
- private long getContributedMediaSize(@NonNull String pkg, @NonNull UserHandle user) {
+ private long getContributedMediaSizeForUser(@NonNull String pkg, @NonNull UserHandle user) {
try {
return MediaStore.getContributedMediaSize(getContext(), pkg, user);
} catch (IOException e) {
+ Log.e(LOG_TAG, "Cannot determine amount of contributes files for " + pkg
+ + " (user " + user + ")", e);
return 0;
}
}
+ /**
+ * Get number of bytes of the files contributed by the package.
+ *
+ * @param pkg The package that might have contributed files.
+ * @param user The user the package belongs to or {@code null} if files of all users should be
+ * counted.
+ *
+ * @return The number of bytes.
+ */
+ private long getContributedMediaSize(@NonNull String pkg, @Nullable UserHandle user) {
+ UserManager userManager = getContext().getSystemService(UserManager.class);
+
+ long contributedFileSize = 0;
+
+ if (user == null) {
+ List users = userManager.getUsers();
+
+ int numUsers = users.size();
+ for (int i = 0; i < numUsers; i++) {
+ contributedFileSize += getContributedMediaSizeForUser(pkg,
+ UserHandle.of(users.get(i).id));
+ }
+ } else {
+ contributedFileSize = getContributedMediaSizeForUser(pkg, user);
+ }
+
+ return contributedFileSize;
+ }
+
+ /**
+ * Get number of bytes of the app data of the package.
+ *
+ * @param pkg The package that might have app data.
+ * @param user The user the package belongs to
+ *
+ * @return The number of bytes.
+ */
+ private long getAppDataSizeForUser(@NonNull String pkg, @NonNull UserHandle user) {
+ StorageManager storageManager = getContext().getSystemService(StorageManager.class);
+ StorageStatsManager storageStatsManager =
+ getContext().getSystemService(StorageStatsManager.class);
+
+ List volumes = storageManager.getStorageVolumes();
+ long appDataSize = 0;
+
+ int numVolumes = volumes.size();
+ for (int i = 0; i < numVolumes; i++) {
+ StorageStats stats;
+ try {
+ stats = storageStatsManager.queryStatsForPackage(convert(volumes.get(i).getUuid()),
+ pkg, user);
+ } catch (PackageManager.NameNotFoundException | IOException e) {
+ Log.e(LOG_TAG, "Cannot determine amount of app data for " + pkg + " on "
+ + volumes.get(i) + " (user " + user + ")", e);
+ continue;
+ }
+
+ appDataSize += stats.getDataBytes();
+ }
+
+ return appDataSize;
+ }
+
+ /**
+ * Get number of bytes of the app data of the package.
+ *
+ * @param pkg The package that might have app data.
+ * @param user The user the package belongs to or {@code null} if files of all users should be
+ * counted.
+ *
+ * @return The number of bytes.
+ */
+ private long getAppDataSize(@NonNull String pkg, @Nullable UserHandle user) {
+ UserManager userManager = getContext().getSystemService(UserManager.class);
+
+ long appDataSize = 0;
+
+ if (user == null) {
+ List users = userManager.getUsers();
+
+ int numUsers = users.size();
+ for (int i = 0; i < numUsers; i++) {
+ appDataSize += getAppDataSizeForUser(pkg, UserHandle.of(users.get(i).id));
+ }
+ } else {
+ appDataSize = getAppDataSizeForUser(pkg, user);
+ }
+
+ return appDataSize;
+ }
+
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final PackageManager pm = getActivity().getPackageManager();
@@ -108,30 +212,46 @@ public class UninstallAlertDialogFragment extends DialogFragment implements
dialogBuilder.setNegativeButton(android.R.string.cancel, this);
String pkg = dialogInfo.appInfo.packageName;
- long contributedFileSize = 0;
- if (dialogInfo.allUsers) {
- List users = userManager.getUsers();
+ long contributedFileSize = getContributedMediaSize(pkg,
+ dialogInfo.allUsers ? null : dialogInfo.user);
- int numUsers = users.size();
- for (int i = 0; i < numUsers; i++) {
- UserHandle user = UserHandle.of(users.get(i).id);
+ boolean suggestToKeepAppData;
+ try {
+ PackageInfo pkgInfo = pm.getPackageInfo(pkg, 0);
- contributedFileSize += getContributedMediaSize(pkg, user);
- }
- } else {
- contributedFileSize = getContributedMediaSize(pkg, dialogInfo.user);
+ suggestToKeepAppData = pkgInfo.applicationInfo.hasFragileUserData();
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(LOG_TAG, "Cannot check hasFragileUserData for " + pkg, e);
+ suggestToKeepAppData = false;
}
- if (contributedFileSize == 0) {
+ long appDataSize = 0;
+ if (suggestToKeepAppData) {
+ appDataSize = getAppDataSize(pkg, dialogInfo.allUsers ? null : dialogInfo.user);
+ }
+
+ if (contributedFileSize == 0 && appDataSize == 0) {
dialogBuilder.setMessage(messageBuilder.toString());
} else {
LayoutInflater inflater = getContext().getSystemService(LayoutInflater.class);
ViewGroup content = (ViewGroup) inflater.inflate(R.layout.uninstall_content_view, null);
((TextView) content.requireViewById(R.id.message)).setText(messageBuilder.toString());
- mClearContributedFiles = content.requireViewById(R.id.checkbox);
- mClearContributedFiles.setText(getString(R.string.uninstall_remove_contributed_files,
- formatFileSize(getContext(), contributedFileSize)));
+
+ if (contributedFileSize != 0) {
+ mClearContributedFiles = content.requireViewById(R.id.clearContributedFiles);
+ mClearContributedFiles.setVisibility(View.VISIBLE);
+ mClearContributedFiles.setText(
+ getString(R.string.uninstall_remove_contributed_files,
+ formatFileSize(getContext(), contributedFileSize)));
+ }
+
+ if (appDataSize != 0) {
+ mKeepData = content.requireViewById(R.id.keepData);
+ mKeepData.setVisibility(View.VISIBLE);
+ mKeepData.setText(getString(R.string.uninstall_keep_data,
+ formatFileSize(getContext(), appDataSize)));
+ }
dialogBuilder.setView(content);
}
@@ -143,7 +263,8 @@ public class UninstallAlertDialogFragment extends DialogFragment implements
public void onClick(DialogInterface dialog, int which) {
if (which == Dialog.BUTTON_POSITIVE) {
((UninstallerActivity) getActivity()).startUninstallProgress(
- mClearContributedFiles != null && mClearContributedFiles.isChecked());
+ mClearContributedFiles != null && mClearContributedFiles.isChecked(),
+ mKeepData != null && mKeepData.isChecked());
} else {
((UninstallerActivity) getActivity()).dispatchAborted();
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java
index 21d25f5b030fd..ac5fd76f5bdad 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java
@@ -99,7 +99,7 @@ public class UninstallAlertFragment extends GuidedStepFragment {
public void onGuidedActionClicked(GuidedAction action) {
if (isAdded()) {
if (action.getId() == GuidedAction.ACTION_ID_OK) {
- ((UninstallerActivity) getActivity()).startUninstallProgress(false);
+ ((UninstallerActivity) getActivity()).startUninstallProgress(false, false);
getActivity().finish();
} else {
((UninstallerActivity) getActivity()).dispatchAborted();