fragileUserData-apps might keep data on uninstall
If an app declares that is has flagile user data, all the user to choose
to keep the app-data on uninstall.
Test: Unistalled apps that set the new flag and app that did not.
Verified that the KEEP_DATA flag was set when checkbox was
clicked.
Change-Id: I032fb21854352bbc175934ae5eb68a1430b1d403
Fixes: 117578306
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user