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)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f6c7957..35914ca 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -81,6 +81,7 @@
     <uses-permission android:name="android.permission.CHANGE_APP_IDLE_STATE" />
     <uses-permission android:name="android.permission.PEERS_MAC_ADDRESS"/>
     <uses-permission android:name="android.permission.MANAGE_NOTIFICATIONS"/>
+    <uses-permission android:name="android.permission.DELETE_PACKAGES"/>
 
     <application android:label="@string/settings_label"
             android:icon="@mipmap/ic_launcher_settings"
diff --git a/res/layout/app_item.xml b/res/layout/app_item.xml
index ea0008e..15a9014 100644
--- a/res/layout/app_item.xml
+++ b/res/layout/app_item.xml
@@ -60,6 +60,14 @@
         android:textColor="?android:attr/textColorSecondary"
         android:duplicateParentState="true" />
 
+    <Switch
+            android:id="@android:id/switch_widget"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:background="@null"
+            android:visibility="gone"/>
+
     <FrameLayout
         android:id="@android:id/widget_frame"
         android:layout_width="wrap_content"
diff --git a/res/menu/storage_volume.xml b/res/menu/storage_volume.xml
index efa468c..bf9f985 100644
--- a/res/menu/storage_volume.xml
+++ b/res/menu/storage_volume.xml
@@ -30,4 +30,7 @@
     <item
         android:id="@+id/storage_migrate"
         android:title="@string/storage_menu_migrate" />
+    <item
+        android:id="@+id/storage_free"
+        android:title="@string/storage_menu_free" />
 </menu>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 8337321..fe8f25b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -2441,6 +2441,8 @@
     <string name="storage_menu_set_up">Set up</string>
     <!-- Storage setting.  Menu option for exploring a storage device [CHAR LIMIT=30]-->
     <string name="storage_menu_explore">Explore</string>
+    <!-- Storage setting. Menu option for using the deletion helper. [CHAR LIMIT=30] -->
+    <string name="storage_menu_free">Free up space</string>
 
     <!-- Storage setting.  Title for USB transfer settings [CHAR LIMIT=30]-->
     <string name="storage_title_usb">USB computer connection</string>
diff --git a/res/xml/deletion_helper_list.xml b/res/xml/deletion_helper_list.xml
new file mode 100644
index 0000000..7979b46
--- /dev/null
+++ b/res/xml/deletion_helper_list.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+                  android:title="@string/deletion_helper_title">
+
+    <PreferenceCategory
+            android:key="apps_group"
+            android:title="@string/deletion_helper_apps_title" />
+
+</PreferenceScreen>
diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java
index 7936cb9..d212968 100644
--- a/src/com/android/settings/applications/ManageApplications.java
+++ b/src/com/android/settings/applications/ManageApplications.java
@@ -81,7 +81,7 @@
 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 0000000..cb025ab
--- /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 0000000..013c801
--- /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<AppEntry> apps = mAppSession.getAllApps();
+        if (apps == null) return;
+
+        final Map<String, UsageStats> 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<String, UsageStats> 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 0000000..9f78b0d
--- /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<String> mUncheckedApplications;
+    private AppStateUsageStatsBridge mDataUsageBridge;
+    private ArrayList<AppEntry> 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<String>) 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<String> 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<AppEntry> 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<AppEntry> 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 0000000..69e6a6c
--- /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<String> mPackages;
+    private Callback mCallback;
+    private PackageManager mPm;
+    private UserHandle mUser;
+
+    public PackageDeletionTask(PackageManager pm, Set<String> 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 66026eb..c81ec53 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.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 @@
         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 @@
             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 @@
                 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 0000000..10ba585
--- /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<String> mDeletedApps;
+
+    @Override
+    protected void setUp() throws Exception {
+        mPackageManager = new FakePackageManager();
+        mDeletedApps = new HashSet<String>();
+    }
+
+    @SmallTest
+    public void testDeleteNoApps() throws Exception {
+        runTask(new HashSet<String>(), false);
+    }
+
+    @SmallTest
+    public void testDeleteOneApp() throws Exception {
+        HashSet<String> appsToDelete = new HashSet<String>();
+        appsToDelete.add("app.test1");
+        runTask(appsToDelete, false);
+    }
+
+    @SmallTest
+    public void testDeleteManyApps() throws Exception {
+        HashSet<String> appsToDelete = new HashSet<String>();
+        appsToDelete.add("app.test1");
+        appsToDelete.add("app.test2");
+        runTask(appsToDelete, false);
+    }
+
+    @SmallTest
+    public void testDeleteFails() throws Exception {
+        HashSet<String> appsToDelete = new HashSet<String>();
+        appsToDelete.add("app.test1");
+        mPackageManager.deletionSucceeds = false;
+        runTask(appsToDelete, true);
+    }
+
+    private void runTask(HashSet<String> 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<String> mExpectedDeletedApps;
+        private boolean mShouldFail;
+
+        public VerifierCallback(HashSet<String> 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);
+        }
+    }
+
+}