Add a collapsible checkbox preference for deletion helper.

This adds a custom preference group which has both a checkbox
and collapse/expand behavior. This is intended to be used in
the deletion helper for apps deletion and downloads folder
deletion.

This patch implements the apps deletion integration.

Bug: 28769691
Change-Id: I9fb28a1baa4067841742b5dbeaf2083728c16144
diff --git a/res/drawable/ic_keyboard_arrow_down_black_32.xml b/res/drawable/ic_keyboard_arrow_down_black_32.xml
new file mode 100644
index 0000000..12ce9c2
--- /dev/null
+++ b/res/drawable/ic_keyboard_arrow_down_black_32.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="32dp"
+        android:height="32dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+            android:fillColor="#FF000000"
+            android:pathData="M7.41,7.84L12,12.42l4.59,-4.58L18,9.25l-6,6 -6,-6z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_keyboard_arrow_up_black_32.xml b/res/drawable/ic_keyboard_arrow_up_black_32.xml
new file mode 100644
index 0000000..d419800
--- /dev/null
+++ b/res/drawable/ic_keyboard_arrow_up_black_32.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="32dp"
+        android:height="32dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+            android:fillColor="#FF000000"
+            android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
+</vector>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6c9e645..10ad16b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7620,4 +7620,10 @@
     <!-- Button to activate the storage manager on the storage manager upsell. [CHAR LIMIT=20]-->
     <string name="deletion_helper_upsell_activate">Turn on</string>
 
+    <!-- Title for the apps category in the deletion helper, showing how many apps to delete. [CHAR LIMIT=40]-->
+    <string name="deletion_helper_apps_group_title">Apps (<xliff:g id="num_items">%1$d</xliff:g>)</string>
+
+    <!-- Summary for the apps category in the deletion helper, showing how many space to clear. [CHAR LIMIT=NONE]-->
+    <string name="deletion_helper_apps_group_summary"><xliff:g id="used" example="1.2GB">%1$s</xliff:g></string>
+
 </resources>
diff --git a/res/xml/deletion_helper_list.xml b/res/xml/deletion_helper_list.xml
index 78d3b14..ac5a851 100644
--- a/res/xml/deletion_helper_list.xml
+++ b/res/xml/deletion_helper_list.xml
@@ -23,8 +23,8 @@
     <com.android.settings.deletionhelper.DownloadsDeletionPreference
         android:key="delete_downloads" />
 
-    <PreferenceCategory
-        android:key="apps_group"
-        android:title="@string/deletion_helper_apps_title" />
+    <com.android.settings.CollapsibleCheckboxPreferenceGroup
+            android:key="apps_group"
+            android:icon="@drawable/ic_keyboard_arrow_down_black_32"/>
 
 </PreferenceScreen>
diff --git a/src/com/android/settings/CollapsibleCheckboxPreferenceGroup.java b/src/com/android/settings/CollapsibleCheckboxPreferenceGroup.java
new file mode 100644
index 0000000..a0f9d98
--- /dev/null
+++ b/src/com/android/settings/CollapsibleCheckboxPreferenceGroup.java
@@ -0,0 +1,147 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceGroup;
+import android.support.v7.preference.PreferenceViewHolder;
+import android.util.AttributeSet;
+
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.Checkable;
+import android.widget.TextView;
+
+import java.util.LinkedHashMap;
+
+/**
+ * CollapsibleCheckboxPreferenceGroup is a preference group that can be expanded or collapsed and
+ * also has a checkbox.
+ */
+public class CollapsibleCheckboxPreferenceGroup extends PreferenceGroup implements
+        View.OnClickListener {
+    private boolean mCollapsed;
+    private boolean mChecked;
+
+    public CollapsibleCheckboxPreferenceGroup(Context context) {
+        this(context, null);
+    }
+
+    public CollapsibleCheckboxPreferenceGroup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setWidgetLayoutResource(com.android.settings.R.layout.preference_widget_checkbox);
+    }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+        View checkbox = holder.findViewById(com.android.internal.R.id.checkbox);
+        if (checkbox != null && checkbox instanceof Checkable) {
+            ((Checkable) checkbox).setChecked(mChecked);
+            checkbox.setClickable(true);
+            checkbox.setFocusable(true);
+            checkbox.setOnClickListener(this);
+        }
+
+        final TextView titleView = (TextView) holder.findViewById(android.R.id.title);
+        if (titleView != null) {
+            Context context = getContext();
+            TypedValue value = new TypedValue();
+            context.getTheme().resolveAttribute(android.R.attr.colorAccent, value, true);
+            titleView.setTextColor(context.getColor(value.resourceId));
+        }
+    }
+
+    @Override
+    public boolean addPreference(Preference p) {
+        super.addPreference(p);
+        p.setVisible(!isCollapsed());
+        return true;
+    }
+
+    // The preference click handler.
+    @Override
+    protected void onClick() {
+        super.onClick();
+        setCollapse(!isCollapsed());
+    }
+
+    // The checkbox view click handler.
+    @Override
+    public void onClick(View v) {
+        setChecked(!isChecked());
+    }
+
+    /**
+     * Return if the view is collapsed.
+     */
+    public boolean isCollapsed() {
+        return mCollapsed;
+    }
+
+    /**
+     * Returns the checked state of the preference.
+     */
+    public boolean isChecked() {
+        return mChecked;
+    }
+
+    /**
+     * Sets the checked state and notifies listeners of the state change.
+     */
+    public void setChecked(boolean checked) {
+        if (mChecked != checked) {
+            mChecked = checked;
+
+            callChangeListener(checked);
+            notifyDependencyChange(shouldDisableDependents());
+            notifyChanged();
+        }
+    }
+
+    private void setCollapse(boolean isCollapsed) {
+        if (mCollapsed == isCollapsed) {
+            return;
+        }
+
+        mCollapsed = isCollapsed;
+        if (isCollapsed) {
+            hideDropdownPreferences();
+        } else {
+            showDropdownPreferences();
+        }
+    }
+
+    private void showDropdownPreferences() {
+        setAllPreferencesVisibility(true);
+        setIcon(R.drawable.ic_keyboard_arrow_down_black_32);
+    }
+
+    private void hideDropdownPreferences() {
+        setAllPreferencesVisibility(false);
+        setIcon(R.drawable.ic_keyboard_arrow_up_black_32);
+    }
+
+    private void setAllPreferencesVisibility(boolean visible) {
+        for (int i = 0; i < getPreferenceCount(); i++) {
+            Preference p = getPreference(i);
+            p.setVisible(visible);
+        }
+    }
+}
diff --git a/src/com/android/settings/deletionhelper/DeletionHelperFragment.java b/src/com/android/settings/deletionhelper/DeletionHelperFragment.java
index 8c08ce7..357db90 100644
--- a/src/com/android/settings/deletionhelper/DeletionHelperFragment.java
+++ b/src/com/android/settings/deletionhelper/DeletionHelperFragment.java
@@ -16,17 +16,18 @@
 
 package com.android.settings.deletionhelper;
 
+import android.app.Activity;
 import android.app.Application;
 import android.content.Intent;
 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.widget.Button;
 import com.android.settings.deletionhelper.DownloadsDeletionPreference;
+import com.android.settings.CollapsibleCheckboxPreferenceGroup;
 import com.android.settings.PhotosDeletionPreference;
 import com.android.settings.SettingsPreferenceFragment;
 import com.android.settings.R;
@@ -65,7 +66,7 @@
     private static final int DOWNLOADS_LOADER_ID = 1;
 
     private Button mCancel, mFree;
-    private PreferenceGroup mApps;
+    private CollapsibleCheckboxPreferenceGroup mApps;
     private PhotosDeletionPreference mPhotoPreference;
     private DownloadsDeletionPreference mDownloadsPreference;
 
@@ -82,6 +83,7 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        setAnimationAllowed(true);
         Application app = getActivity().getApplication();
         mState = ApplicationsState.getInstance(app);
         mSession = mState.newSession(this);
@@ -89,7 +91,7 @@
         mDataUsageBridge = new AppStateUsageStatsBridge(getActivity(), mState, this);
 
         addPreferencesFromResource(R.xml.deletion_helper_list);
-        mApps = (PreferenceGroup) findPreference(KEY_APPS_GROUP);
+        mApps = (CollapsibleCheckboxPreferenceGroup) findPreference(KEY_APPS_GROUP);
         mPhotoPreference = (PhotosDeletionPreference) findPreference(KEY_PHOTOS_VIDEOS_PREFERENCE);
         mDownloadsPreference =
                 (DownloadsDeletionPreference) findPreference(KEY_DOWNLOADS_PREFERENCE);
@@ -131,6 +133,7 @@
 
         mDownloadsPreference.registerFreeableChangedListener(this);
         mDownloadsPreference.registerDeletionService(mDownloadsDeletion);
+        mApps.setOnPreferenceChangeListener(this);
     }
 
     @Override
@@ -191,24 +194,7 @@
                         ApplicationsState.SIZE_COMPARATOR);
         if (apps == null) return;
         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(mCheckedApplications.contains(packageName));
-                preference.setOnPreferenceChangeListener(this);
-                mApps.addPreference(preference);
-            }
-            preference.setOrder(i);
-        }
-        removeCachedPrefs(mApps);
+        refreshAppGroup(apps);
 
         // 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.
@@ -277,11 +263,20 @@
     @Override
     public boolean onPreferenceChange(Preference preference, Object newValue) {
         boolean checked = (boolean) newValue;
+        if (preference.getKey().equals(mApps.getKey())) {
+            return toggleAllApps(checked);
+        }
+
         String packageName = ((AppDeletionPreference) preference).getPackageName();
         if (checked) {
             mCheckedApplications.add(packageName);
         } else {
             mCheckedApplications.remove(packageName);
+
+            // We remove the preference change listener to avoid toggling every app on and off.
+            mApps.setOnPreferenceChangeListener(null);
+            mApps.setChecked(false);
+            mApps.setOnPreferenceChangeListener(this);
         }
         updateFreeButtonText();
         return true;
@@ -346,17 +341,7 @@
 
     private long getTotalFreeableSpace() {
         long freeableSpace = 0;
-        if (mAppEntries != null) {
-
-            for (int i = 0; i < mAppEntries.size(); i++) {
-                final AppEntry entry = mAppEntries.get(i);
-                long entrySize = mAppEntries.get(i).size;
-                // If the entrySize is negative, it is either an unknown size or an error occurred.
-                if (mCheckedApplications.contains(entry.label) && entrySize > 0) {
-                    freeableSpace += entrySize;
-                }
-            }
-        }
+        freeableSpace += getTotalAppsFreeableSpace(false);
         if (mPhotoPreference != null) {
             freeableSpace += mPhotoPreference.getFreeableBytes();
         }
@@ -365,4 +350,67 @@
         }
         return freeableSpace;
     }
+
+    private void refreshAppGroup(ArrayList<AppEntry> apps) {
+        int entryCount = apps.size();
+        cacheRemoveAllPrefs(mApps);
+        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.setOnPreferenceChangeListener(this);
+                mApps.addPreference(preference);
+            }
+            preference.setChecked(mCheckedApplications.contains(packageName));
+            preference.setOrder(i);
+        }
+        removeCachedPrefs(mApps);
+        updateAppsGroupText();
+    }
+
+    private long getTotalAppsFreeableSpace(boolean countUnchecked) {
+        long freeableSpace = 0;
+        if (mAppEntries != null) {
+            for (int i = 0; i < mAppEntries.size(); i++) {
+                final AppEntry entry = mAppEntries.get(i);
+                long entrySize = mAppEntries.get(i).size;
+                // If the entrySize is negative, it is either an unknown size or an error occurred.
+                if ((countUnchecked ||
+                        mCheckedApplications.contains(entry.label)) && entrySize > 0) {
+                    freeableSpace += entrySize;
+                }
+            }
+        }
+
+        return freeableSpace;
+    }
+
+    private void updateAppsGroupText() {
+        if (mAppEntries != null) {
+            Activity app = getActivity();
+            mApps.setTitle(app.getString(R.string.deletion_helper_apps_group_title,
+                    mAppEntries.size()));
+            mApps.setSummary(app.getString(R.string.deletion_helper_apps_group_summary,
+                    Formatter.formatFileSize(app,
+                            getTotalAppsFreeableSpace(true))));
+        }
+    }
+
+    private boolean toggleAllApps(boolean checked) {
+        for (AppEntry entry : mAppEntries) {
+            final String packageName = entry.label;
+            if (checked) {
+                mCheckedApplications.add(packageName);
+            } else {
+                mCheckedApplications.remove(packageName);
+            }
+        }
+        refreshAppGroup(mAppEntries);
+        updateFreeButtonText();
+        return true;
+    }
 }
\ No newline at end of file