diff options
13 files changed, 193 insertions, 22 deletions
diff --git a/api/current.txt b/api/current.txt index e600870d1a71..f1f37896a3db 100644 --- a/api/current.txt +++ b/api/current.txt @@ -704,6 +704,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 07d6e4785759..c361ac12667e 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 d0de9a1d2a76..61a74ded02d0 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 ab4bd0534025..35263a3fa891 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1603,6 +1603,9 @@ <!-- Declares that this application should be invoked without non-SDK API enforcement --> <attr name="usesNonSdkApi" /> + <!-- If {@code true} the user is prompted to keep the app's data on uninstall --> + <attr name="hasFragileUserData" /> + </declare-styleable> <!-- The <code>permission</code> tag declares a security permission that can be used to control access from other packages to specific components or diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index d480121fc998..05a5bed1da7f 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2930,6 +2930,7 @@ <public name="dataRetentionTime" /> <public name="selectionDividerHeight" /> <public name="foregroundServiceType" /> + <public name="hasFragileUserData" /> </public-group> <public-group type="drawable" first-id="0x010800b4"> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 0a2f057ccb69..dcf95fdbb438 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -142,6 +142,7 @@ applications that come with the platform <permission name="android.permission.UPDATE_APP_OPS_STATS"/> <permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/> <permission name="android.permission.CLEAR_APP_USER_DATA"/> + <permission name="android.permission.PACKAGE_USAGE_STATS"/> </privapp-permissions> <privapp-permissions package="com.android.permissioncontroller"> diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index 18b86628ea4d..591cf7071e01 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -16,6 +16,7 @@ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" /> <uses-permission android:name="android.permission.CLEAR_APP_USER_DATA" /> + <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> <uses-permission android:name="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" /> diff --git a/packages/PackageInstaller/res/layout/uninstall_content_view.xml b/packages/PackageInstaller/res/layout/uninstall_content_view.xml index 5ecb614e7209..2f8966c0461b 100644 --- a/packages/PackageInstaller/res/layout/uninstall_content_view.xml +++ b/packages/PackageInstaller/res/layout/uninstall_content_view.xml @@ -36,12 +36,23 @@ style="@android:style/TextAppearance.Material.Subhead" /> <CheckBox - android:id="@+id/checkbox" + android:id="@+id/clearContributedFiles" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginStart="-8dp" android:paddingLeft="8sp" + android:visibility="gone" + style="@android:style/TextAppearance.Material.Subhead" /> + + <CheckBox + android:id="@+id/keepData" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginStart="-8dp" + android:paddingLeft="8sp" + android:visibility="gone" style="@android:style/TextAppearance.Material.Subhead" /> </LinearLayout>
\ No newline at end of file diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml index 1d8747a1fda6..1e0ff506cb20 100644 --- a/packages/PackageInstaller/res/values/strings.xml +++ b/packages/PackageInstaller/res/values/strings.xml @@ -121,6 +121,8 @@ <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> <!-- Label of a checkbox that allows to remove the files contributed by app during uninstall [CHAR LIMIT=none] --> <string name="uninstall_remove_contributed_files">Also remove <xliff:g id="size" example="1.5MB">%1$s</xliff:g> of associated media files.</string> + <!-- Label of a checkbox that allows to remove the files contributed by app during uninstall [CHAR LIMIT=none] --> + <string name="uninstall_keep_data">Keep <xliff:g id="size" example="1.5MB">%1$s</xliff:g> of app data.</string> <!-- Label for the notification channel containing notifications for current uninstall operations [CHAR LIMIT=40] --> <string name="uninstalling_notification_channel">Running uninstalls</string> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java index d13bb65604af..63d8c5a82519 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 0fa8c9a688c7..54194491d140 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 e4e12759211d..499da758739e 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<UserInfo> 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<StorageVolume> 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<UserInfo> 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<UserInfo> 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; + } + + long appDataSize = 0; + if (suggestToKeepAppData) { + appDataSize = getAppDataSize(pkg, dialogInfo.allUsers ? null : dialogInfo.user); } - if (contributedFileSize == 0) { + 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 21d25f5b030f..ac5fd76f5bda 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(); |