From b84304d4e610eef74cd844254e489962969840c2 Mon Sep 17 00:00:00 2001 From: Daniel Nishi Date: Mon, 11 Apr 2016 14:08:44 -0700 Subject: [PATCH] Add a deletion helper view for clearing space. This view provides an interface which allows users to clear out apps and other data which they may no longer need to alleviate storage pressure. This implementation gives the bits to uninstall apps which have not been used within the last 60 days from the system. A future patch will add in the ability to extend the deletion helper view to have additional clearing types. Change-Id: I65cba54ca247d52227e67e2fa9b84fe63fb8ab34 (cherry picked from commit a671272bf6a48c2b25e4c3f6e823753df674506f) --- AndroidManifest.xml | 1 + res/layout/app_item.xml | 8 + res/menu/storage_volume.xml | 3 + res/values/strings.xml | 2 + res/xml/deletion_helper_list.xml | 24 ++ .../applications/ManageApplications.java | 2 +- .../deletionhelper/AppDeletionPreference.java | 90 ++++++ .../AppStateUsageStatsBridge.java | 109 ++++++++ .../DeletionHelperFragment.java | 262 ++++++++++++++++++ .../deletionhelper/PackageDeletionTask.java | 58 ++++ .../deviceinfo/PrivateVolumeSettings.java | 8 + .../PackageDeletionTaskTest.java | 120 ++++++++ 12 files changed, 686 insertions(+), 1 deletion(-) create mode 100644 res/xml/deletion_helper_list.xml create mode 100644 src/com/android/settings/deletionhelper/AppDeletionPreference.java create mode 100644 src/com/android/settings/deletionhelper/AppStateUsageStatsBridge.java create mode 100644 src/com/android/settings/deletionhelper/DeletionHelperFragment.java create mode 100644 src/com/android/settings/deletionhelper/PackageDeletionTask.java create mode 100644 tests/unit/src/com/android/settings/deletionhelper/PackageDeletionTaskTest.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f6c79572f43..35914caf6d7 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); + } + } + +}