diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1952fbb16c7..8f973d032aa 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -81,6 +81,7 @@
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 8337321a1f8..fe8f25bc55d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -2441,6 +2441,8 @@
Set up
Explore
+
+ Free up space
USB computer connection
diff --git a/res/xml/deletion_helper_list.xml b/res/xml/deletion_helper_list.xml
new file mode 100644
index 00000000000..7979b46c1c5
--- /dev/null
+++ b/res/xml/deletion_helper_list.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java
index 7936cb96494..d2129685338 100644
--- a/src/com/android/settings/applications/ManageApplications.java
+++ b/src/com/android/settings/applications/ManageApplications.java
@@ -81,7 +81,7 @@ import com.android.settingslib.applications.ApplicationsState.VolumeFilter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
-import java.util.Locale;
+import java.util.Locale;
/**
* Activity to pick an application that will be used to display installation information and
diff --git a/src/com/android/settings/deletionhelper/AppDeletionPreference.java b/src/com/android/settings/deletionhelper/AppDeletionPreference.java
new file mode 100644
index 00000000000..cb025abe637
--- /dev/null
+++ b/src/com/android/settings/deletionhelper/AppDeletionPreference.java
@@ -0,0 +1,90 @@
+/*
+ * 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.settings.deletionhelper;
+
+import android.content.Context;
+import android.support.v14.preference.SwitchPreference;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceViewHolder;
+import android.text.format.Formatter;
+import android.view.View;
+import android.widget.Switch;
+import android.widget.TextView;
+import com.android.settings.R;
+
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+
+/**
+ * Preference item for an app with a switch to signify if it should be uninstalled.
+ * This shows the name and icon of the app along with the days since its last use.
+ */
+public class AppDeletionPreference extends SwitchPreference {
+ private AppEntry mEntry;
+ private Context mContext;
+
+ public AppDeletionPreference(Context context, AppEntry item, ApplicationsState state) {
+ super(context);
+ mEntry = item;
+ mContext = context;
+ setLayoutResource(com.android.settings.R.layout.preference_app);
+ setWidgetLayoutResource(R.layout.widget_text_views);
+
+ synchronized (item) {
+ state.ensureIcon(item);
+ if (item.icon != null)
+ setIcon(item.icon);
+ if (item.label != null)
+ setTitle(item.label);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+ Switch switchWidget = (Switch) holder.findViewById(com.android.internal.R.id.switch_widget);
+ switchWidget.setVisibility(View.VISIBLE);
+
+ TextView summary = (TextView) holder.findViewById(R.id.widget_text1);
+ updateSummaryText(summary);
+ }
+
+ public String getPackageName() {
+ return mEntry.label;
+ }
+
+ private void updateSummaryText(TextView summary) {
+ if (mEntry.extraInfo == null) return;
+ if (mEntry.size == ApplicationsState.SIZE_UNKNOWN ||
+ mEntry.size == ApplicationsState.SIZE_INVALID) {
+ return;
+ }
+
+ long daysSinceLastUse = (long) mEntry.extraInfo;
+ String fileSize = Formatter.formatFileSize(mContext, mEntry.size);
+ if (daysSinceLastUse == AppStateUsageStatsBridge.NEVER_USED) {
+ summary.setText(mContext.getString(R.string.deletion_helper_app_summary_never_used,
+ fileSize));
+ } else if (daysSinceLastUse == AppStateUsageStatsBridge.UNKNOWN_LAST_USE) {
+ summary.setText(mContext.getString(R.string.deletion_helper_app_summary_unknown_used,
+ fileSize));
+ } else {
+ summary.setText(mContext.getString(R.string.deletion_helper_app_summary,
+ fileSize,
+ daysSinceLastUse));
+ }
+ }
+
+}
diff --git a/src/com/android/settings/deletionhelper/AppStateUsageStatsBridge.java b/src/com/android/settings/deletionhelper/AppStateUsageStatsBridge.java
new file mode 100644
index 00000000000..013c801989c
--- /dev/null
+++ b/src/com/android/settings/deletionhelper/AppStateUsageStatsBridge.java
@@ -0,0 +1,109 @@
+/*
+ * 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.settings.deletionhelper;
+
+import android.app.usage.UsageStats;
+import android.app.usage.UsageStatsManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+
+import com.android.settings.applications.AppStateBaseBridge;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+import com.android.settingslib.applications.ApplicationsState.AppFilter;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Connects data from the UsageStatsManager to the ApplicationsState.
+ */
+public class AppStateUsageStatsBridge extends AppStateBaseBridge {
+ private UsageStatsManager mUsageStatsManager;
+ public static final long NEVER_USED = -1;
+ public static final long UNKNOWN_LAST_USE = -2;
+
+ public AppStateUsageStatsBridge(Context context, ApplicationsState appState,
+ Callback callback) {
+ super(appState, callback);
+ mUsageStatsManager =
+ (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
+ }
+
+ @Override
+ protected void loadAllExtraInfo() {
+ ArrayList apps = mAppSession.getAllApps();
+ if (apps == null) return;
+
+ final Map map = mUsageStatsManager.queryAndAggregateUsageStats(0,
+ System.currentTimeMillis());
+ for (AppEntry entry : apps) {
+ UsageStats usageStats = map.get(entry.info.packageName);
+ entry.extraInfo = getDaysSinceLastUse(usageStats);
+ }
+ }
+
+ @Override
+ protected void updateExtraInfo(AppEntry app, String pkg, int uid) {
+ Map map = mUsageStatsManager.queryAndAggregateUsageStats(0,
+ System.currentTimeMillis());
+ UsageStats usageStats = map.get(app.info.packageName);
+ app.extraInfo = getDaysSinceLastUse(usageStats);
+ }
+
+ private long getDaysSinceLastUse(UsageStats stats) {
+ if (stats == null) {
+ return NEVER_USED;
+ }
+ long lastUsed = stats.getLastTimeUsed();
+ // Sometimes, a usage is recorded without a time and we don't know when the use was.
+ if (lastUsed == 0) {
+ return UNKNOWN_LAST_USE;
+ }
+ return TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - lastUsed);
+
+ }
+
+ /**
+ * Filters only non-system apps which haven't been used in the last 60 days. If an app's last
+ * usage is unknown, it is skipped.
+ */
+ public static final AppFilter FILTER_USAGE_STATS = new AppFilter() {
+ private long UNUSED_DAYS_DELETION_THRESHOLD = 60;
+
+ @Override
+ public void init() {
+ }
+
+ @Override
+ public boolean filterApp(AppEntry info) {
+ if (info == null) return false;
+ boolean isBundled = (info.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+ return isExtraInfoValid(info.extraInfo) && !isBundled;
+ }
+
+ private boolean isExtraInfoValid(Object extraInfo) {
+ if (extraInfo == null || !(extraInfo instanceof Long)) {
+ return false;
+ }
+
+ long daysSinceLastUse = (long) extraInfo;
+ return daysSinceLastUse >= UNUSED_DAYS_DELETION_THRESHOLD ||
+ daysSinceLastUse == NEVER_USED;
+ }
+ };
+}
diff --git a/src/com/android/settings/deletionhelper/DeletionHelperFragment.java b/src/com/android/settings/deletionhelper/DeletionHelperFragment.java
new file mode 100644
index 00000000000..9f78b0dbaa6
--- /dev/null
+++ b/src/com/android/settings/deletionhelper/DeletionHelperFragment.java
@@ -0,0 +1,262 @@
+package com.android.settings.deletionhelper;
+
+import android.os.Bundle;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceGroup;
+import android.text.format.Formatter;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import com.android.settings.SettingsPreferenceFragment;
+import com.android.settings.R;
+import com.android.internal.logging.MetricsProto.MetricsEvent;
+import com.android.settings.applications.AppStateBaseBridge;
+import com.android.settings.deletionhelper.AppStateUsageStatsBridge;
+import com.android.settings.deletionhelper.AppDeletionPreference;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+import com.android.settingslib.applications.ApplicationsState.Callbacks;
+import com.android.settingslib.applications.ApplicationsState.Session;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+
+/**
+ * Settings screen for the deletion helper, which manually removes data which is not recently used.
+ */
+public class DeletionHelperFragment extends SettingsPreferenceFragment implements
+ ApplicationsState.Callbacks, AppStateBaseBridge.Callback, Preference.OnPreferenceChangeListener {
+ private static final String TAG = "DeletionHelperFragment";
+
+ private static final String EXTRA_HAS_BRIDGE = "hasBridge";
+ private static final String EXTRA_HAS_SIZES = "hasSizes";
+ private static final String EXTRA_CHECKED_SET = "checkedSet";
+
+ private static final String KEY_APPS_GROUP = "apps_group";
+
+ private Button mCancel, mFree;
+ private PreferenceGroup mApps;
+
+ private ApplicationsState mState;
+ private Session mSession;
+ private HashSet mUncheckedApplications;
+ private AppStateUsageStatsBridge mDataUsageBridge;
+ private ArrayList mAppEntries;
+ private boolean mHasReceivedAppEntries, mHasReceivedBridgeCallback, mFinishedLoading;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mState = ApplicationsState.getInstance(getActivity().getApplication());
+ mSession = mState.newSession(this);
+ mUncheckedApplications = new HashSet<>();
+ mDataUsageBridge = new AppStateUsageStatsBridge(getActivity(), mState, this);
+
+ addPreferencesFromResource(R.xml.deletion_helper_list);
+ mApps = (PreferenceGroup) findPreference(KEY_APPS_GROUP);
+
+ if (savedInstanceState != null) {
+ mHasReceivedAppEntries =
+ savedInstanceState.getBoolean(EXTRA_HAS_SIZES, false);
+ mHasReceivedBridgeCallback =
+ savedInstanceState.getBoolean(EXTRA_HAS_BRIDGE, false);
+ mUncheckedApplications =
+ (HashSet) savedInstanceState.getSerializable(EXTRA_CHECKED_SET);
+ }
+ }
+
+ private void initializeButtons(View v) {
+ mCancel = (Button) v.findViewById(R.id.back_button);
+ mCancel.setText(R.string.cancel);
+ mCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finishFragment();
+ }
+ });
+
+ mFree = (Button) v.findViewById(R.id.next_button);
+ mFree.setText(R.string.storage_menu_free);
+ mFree.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ArraySet apps = new ArraySet<>();
+ for (AppEntry entry : mAppEntries) {
+ if (!mUncheckedApplications.contains(entry.label)) {
+ synchronized (entry) {
+ apps.add(entry.info.packageName);
+ }
+ }
+ }
+ // TODO: If needed, add an action on the callback.
+ PackageDeletionTask task = new PackageDeletionTask(
+ getActivity().getPackageManager(), apps,
+ new PackageDeletionTask.Callback() {
+ @Override
+ public void onSuccess() {
+ }
+
+ @Override
+ public void onError() {
+ Log.e(TAG, "An error occurred while uninstalling packages.");
+ }
+ });
+ finishFragment();
+ task.run();
+ }
+ });
+ }
+
+ @Override
+ public void onViewCreated(View v, Bundle savedInstanceState) {
+ super.onViewCreated(v, savedInstanceState);
+ initializeButtons(v);
+ setLoading(true, false);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mSession.resume();
+ mDataUsageBridge.resume();
+ }
+
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(EXTRA_HAS_SIZES, mHasReceivedAppEntries);
+ outState.putBoolean(EXTRA_HAS_BRIDGE, mHasReceivedBridgeCallback);
+ outState.putSerializable(EXTRA_CHECKED_SET, mUncheckedApplications);
+ }
+
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mDataUsageBridge.pause();
+ mSession.pause();
+ }
+
+ private void rebuild() {
+ // Only rebuild if we have the packages and their usage stats.
+ if (!mHasReceivedBridgeCallback || !mHasReceivedAppEntries) {
+ return;
+ }
+
+ final ArrayList apps =
+ mSession.rebuild(AppStateUsageStatsBridge.FILTER_USAGE_STATS,
+ ApplicationsState.SIZE_COMPARATOR);
+ mAppEntries = apps;
+ cacheRemoveAllPrefs(mApps);
+ int entryCount = apps.size();
+ for (int i = 0; i < entryCount; i++) {
+ AppEntry entry = apps.get(i);
+ final String packageName = entry.label;
+ AppDeletionPreference preference = (AppDeletionPreference) getCachedPreference(entry.label);
+ if (preference == null) {
+ preference = new AppDeletionPreference(getActivity(), entry,
+ mState);
+ preference.setKey(packageName);
+ preference.setChecked(!mUncheckedApplications.contains(packageName));
+ preference.setOnPreferenceChangeListener(this);
+ mApps.addPreference(preference);
+ }
+ preference.setOrder(i);
+ }
+ removeCachedPrefs(mApps);
+
+ // All applications should be filled in if we've received the sizes.
+ // setLoading being called multiple times causes flickering, so we only do it once.
+ if (mHasReceivedAppEntries && !mFinishedLoading) {
+ mFinishedLoading = true;
+ setLoading(false, true);
+ getButtonBar().setVisibility(View.VISIBLE);
+ }
+ updateFreeButtonText();
+ }
+
+ private void updateFreeButtonText() {
+ mFree.setText(String.format(getActivity().getString(R.string.deletion_helper_free_button),
+ Formatter.formatFileSize(getActivity(), getTotalFreeableSpace())));
+ }
+
+ @Override
+ public void onRunningStateChanged(boolean running) {
+ // No-op.
+ }
+
+ @Override
+ public void onPackageListChanged() {
+ rebuild();
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList apps) {
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) {
+ rebuild();
+ }
+
+ @Override
+ public void onAllSizesComputed() {
+ rebuild();
+ }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ mHasReceivedAppEntries = true;
+ rebuild();
+ }
+
+ @Override
+ public void onExtraInfoUpdated() {
+ mHasReceivedBridgeCallback = true;
+ rebuild();
+ }
+
+ @Override
+ protected int getMetricsCategory() {
+ return MetricsEvent.DEVICEINFO_STORAGE;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ boolean checked = (boolean) newValue;
+ String packageName = ((AppDeletionPreference) preference).getPackageName();
+ if (checked) {
+ mUncheckedApplications.remove(packageName);
+ } else {
+ mUncheckedApplications.add(packageName);
+ }
+ updateFreeButtonText();
+ return true;
+ }
+
+ private long getTotalFreeableSpace() {
+ long freeableSpace = 0;
+ for (int i = 0; i < mAppEntries.size(); i++) {
+ final AppEntry entry = mAppEntries.get(i);
+ if (!mUncheckedApplications.contains(entry.label)) {
+ freeableSpace += mAppEntries.get(i).size;
+ }
+ }
+ return freeableSpace;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/deletionhelper/PackageDeletionTask.java b/src/com/android/settings/deletionhelper/PackageDeletionTask.java
new file mode 100644
index 00000000000..69e6a6cb384
--- /dev/null
+++ b/src/com/android/settings/deletionhelper/PackageDeletionTask.java
@@ -0,0 +1,58 @@
+package com.android.settings.deletionhelper;
+
+import android.content.pm.IPackageDeleteObserver;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Deletes a specified set of apps as a specified user and calls back once done.
+ */
+public class PackageDeletionTask {
+ private Set mPackages;
+ private Callback mCallback;
+ private PackageManager mPm;
+ private UserHandle mUser;
+
+ public PackageDeletionTask(PackageManager pm, Set packageNames, Callback callback) {
+ mPackages = packageNames;
+ mCallback = callback;
+ mPm = pm;
+ mUser = android.os.Process.myUserHandle();
+ }
+
+ public void run() {
+ PackageDeletionObserver observer = new PackageDeletionObserver(mPackages.size());
+ for (String packageName : mPackages) {
+ mPm.deletePackageAsUser(packageName, observer, 0, mUser.getIdentifier());
+ }
+ }
+
+ private class PackageDeletionObserver extends IPackageDeleteObserver.Stub {
+ private final AtomicInteger mPackagesRemaining = new AtomicInteger(0);
+
+ public PackageDeletionObserver(int packages) {
+ mPackagesRemaining.set(packages);
+ }
+
+ @Override
+ public void packageDeleted(String packageName, int returnCode) {
+ if (returnCode != PackageManager.DELETE_SUCCEEDED) {
+ mCallback.onError();
+ return;
+ }
+
+ int remaining = mPackagesRemaining.decrementAndGet();
+ if (remaining == 0) {
+ mCallback.onSuccess();
+ }
+ }
+ }
+
+ public static abstract class Callback {
+ public abstract void onSuccess();
+ public abstract void onError();
+ }
+}
diff --git a/src/com/android/settings/deviceinfo/PrivateVolumeSettings.java b/src/com/android/settings/deviceinfo/PrivateVolumeSettings.java
index 66026ebf250..c81ec53a806 100644
--- a/src/com/android/settings/deviceinfo/PrivateVolumeSettings.java
+++ b/src/com/android/settings/deviceinfo/PrivateVolumeSettings.java
@@ -58,6 +58,7 @@ import com.android.settings.Settings.StorageUseActivity;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.Utils;
import com.android.settings.applications.ManageApplications;
+import com.android.settings.deletionhelper.DeletionHelperFragment;
import com.android.settings.deviceinfo.StorageSettings.MountTask;
import com.android.settingslib.deviceinfo.StorageMeasurement;
import com.android.settingslib.deviceinfo.StorageMeasurement.MeasurementDetails;
@@ -361,6 +362,7 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment {
final MenuItem unmount = menu.findItem(R.id.storage_unmount);
final MenuItem format = menu.findItem(R.id.storage_format);
final MenuItem migrate = menu.findItem(R.id.storage_migrate);
+ final MenuItem manage = menu.findItem(R.id.storage_free);
// Actions live in menu for non-internal private volumes; they're shown
// as preference items for public volumes.
@@ -369,11 +371,13 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment {
mount.setVisible(false);
unmount.setVisible(false);
format.setVisible(false);
+ manage.setVisible(true);
} else {
rename.setVisible(mVolume.getType() == VolumeInfo.TYPE_PRIVATE);
mount.setVisible(mVolume.getState() == VolumeInfo.STATE_UNMOUNTED);
unmount.setVisible(mVolume.isMountedReadable());
format.setVisible(true);
+ manage.setVisible(false);
}
format.setTitle(R.string.storage_menu_format_public);
@@ -412,6 +416,10 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment {
intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
startActivity(intent);
return true;
+ case R.id.storage_free:
+ startFragment(this, DeletionHelperFragment.class.getCanonicalName(),
+ R.string.deletion_helper_title, 0, args);
+ return true;
}
return super.onOptionsItemSelected(item);
}
diff --git a/tests/unit/src/com/android/settings/deletionhelper/PackageDeletionTaskTest.java b/tests/unit/src/com/android/settings/deletionhelper/PackageDeletionTaskTest.java
new file mode 100644
index 00000000000..10ba5850cc0
--- /dev/null
+++ b/tests/unit/src/com/android/settings/deletionhelper/PackageDeletionTaskTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.settings.deletionhelper;
+
+import android.test.AndroidTestCase;
+import android.content.pm.IPackageDeleteObserver;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.test.mock.MockPackageManager;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.settings.deletionhelper.PackageDeletionTask;
+import com.android.settings.deletionhelper.PackageDeletionTask.Callback;
+
+import java.util.Set;
+import java.util.HashSet;
+
+public class PackageDeletionTaskTest extends AndroidTestCase {
+ private FakePackageManager mPackageManager;
+ private Set mDeletedApps;
+
+ @Override
+ protected void setUp() throws Exception {
+ mPackageManager = new FakePackageManager();
+ mDeletedApps = new HashSet();
+ }
+
+ @SmallTest
+ public void testDeleteNoApps() throws Exception {
+ runTask(new HashSet(), false);
+ }
+
+ @SmallTest
+ public void testDeleteOneApp() throws Exception {
+ HashSet appsToDelete = new HashSet();
+ appsToDelete.add("app.test1");
+ runTask(appsToDelete, false);
+ }
+
+ @SmallTest
+ public void testDeleteManyApps() throws Exception {
+ HashSet appsToDelete = new HashSet();
+ appsToDelete.add("app.test1");
+ appsToDelete.add("app.test2");
+ runTask(appsToDelete, false);
+ }
+
+ @SmallTest
+ public void testDeleteFails() throws Exception {
+ HashSet appsToDelete = new HashSet();
+ appsToDelete.add("app.test1");
+ mPackageManager.deletionSucceeds = false;
+ runTask(appsToDelete, true);
+ }
+
+ private void runTask(HashSet appsToDelete, boolean shouldFail) {
+ PackageDeletionTask task = new PackageDeletionTask(mPackageManager, appsToDelete,
+ new VerifierCallback(appsToDelete, shouldFail));
+ task.run();
+ }
+
+ class FakePackageManager extends MockPackageManager {
+ public boolean deletionSucceeds = true;
+
+ @Override
+ public void deletePackageAsUser(String packageName, IPackageDeleteObserver observer,
+ int flags, int userId) {
+ int resultCode;
+ if (deletionSucceeds) {
+ resultCode = PackageManager.DELETE_SUCCEEDED;
+ mDeletedApps.add(packageName);
+ } else {
+ resultCode = PackageManager.DELETE_FAILED_INTERNAL_ERROR;
+ }
+
+ try {
+ observer.packageDeleted(packageName, resultCode);
+ } catch (RemoteException e) {
+ fail(e.toString());
+ }
+ }
+ }
+
+ class VerifierCallback extends Callback {
+ private Set mExpectedDeletedApps;
+ private boolean mShouldFail;
+
+ public VerifierCallback(HashSet expectedDeletedApps, boolean shouldFail) {
+ mExpectedDeletedApps = expectedDeletedApps;
+ mShouldFail = shouldFail;
+ }
+
+ @Override
+ public void onSuccess() {
+ System.out.println("lol");
+ assertFalse(mShouldFail);
+ assertEquals(mExpectedDeletedApps, mDeletedApps);
+ }
+
+ @Override
+ public void onError() {
+ assertTrue(mShouldFail);
+ }
+ }
+
+}