Merge "Add a loading screen to the Deletion Helper." into oc-dr1-dev
diff --git a/res/layout/loading_container.xml b/res/layout/loading_container.xml
new file mode 100644
index 0000000..592bd6a
--- /dev/null
+++ b/res/layout/loading_container.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/loading_container"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:visibility="gone"
+    android:gravity="center">
+
+  <ProgressBar style="?android:attr/progressBarStyleLarge"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_gravity="center"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/settings_main_prefs.xml b/res/layout/settings_main_prefs.xml
index bd7b0bb..430d409 100644
--- a/res/layout/settings_main_prefs.xml
+++ b/res/layout/settings_main_prefs.xml
@@ -28,6 +28,8 @@
         <include android:id="@+id/empty_state"
             layout="@layout/deletion_helper_empty_state"/>
 
+        <include layout="@layout/loading_container" />
+
         <FrameLayout
             android:id="@+id/main_content"
             android:layout_width="match_parent"
diff --git a/robotests/src/com/android/storagemanager/deletionhelper/LoadingSpinnerControllerTest.java b/robotests/src/com/android/storagemanager/deletionhelper/LoadingSpinnerControllerTest.java
new file mode 100644
index 0000000..6c9414d
--- /dev/null
+++ b/robotests/src/com/android/storagemanager/deletionhelper/LoadingSpinnerControllerTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 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.storagemanager.deletionhelper;
+
+import android.view.View;
+
+import com.android.storagemanager.testing.StorageManagerRobolectricTestRunner;
+import com.android.storagemanager.testing.TestingConstants;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.annotation.Config;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(StorageManagerRobolectricTestRunner.class)
+@Config(manifest = TestingConstants.MANIFEST, sdk = TestingConstants.SDK_VERSION)
+public class LoadingSpinnerControllerTest {
+    @Mock DeletionHelperActivity mActivity;
+    @Mock View mListView;
+    LoadingSpinnerController mController;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mController = new LoadingSpinnerController(mActivity);
+        doAnswer(
+                        new Answer() {
+                            @Override
+                            public Object answer(InvocationOnMock invocationOnMock)
+                                    throws Throwable {
+                                final boolean isLoading =
+                                        (boolean) (invocationOnMock.getArguments())[1];
+                                when(mActivity.isLoadingVisible()).thenReturn(isLoading);
+                                return null;
+                            }
+                        })
+                .when(mActivity)
+                .setLoading(any(View.class), anyBoolean(), anyBoolean());
+    }
+
+    @Test
+    public void neverLoadIfCategoryLoadsBeforeInitialized() {
+        mController.onCategoryLoad();
+        mController.initializeLoading(mListView);
+
+        // Loading should never have been shown.
+        verify(mActivity, never()).setLoading(any(View.class), eq(true), anyBoolean());
+    }
+
+    @Test
+    public void loadUntilCategoriesLoaded() {
+        mController.initializeLoading(mListView);
+        verify(mActivity, never()).setLoading(any(View.class), eq(false), anyBoolean());
+
+        mController.onCategoryLoad();
+        verify(mActivity).setLoading(any(View.class), eq(false), anyBoolean());
+    }
+
+    @Test
+    public void loadingMultipleCategoriesDoesntCauseFlicker() {
+        mController.initializeLoading(mListView);
+
+        mController.onCategoryLoad();
+        mController.onCategoryLoad();
+        mController.onCategoryLoad();
+
+        verify(mActivity, times(1)).setLoading(any(View.class), eq(false), anyBoolean());
+    }
+}
diff --git a/src/com/android/storagemanager/deletionhelper/DeletionHelperActivity.java b/src/com/android/storagemanager/deletionhelper/DeletionHelperActivity.java
index fe19c2b..5255382 100644
--- a/src/com/android/storagemanager/deletionhelper/DeletionHelperActivity.java
+++ b/src/com/android/storagemanager/deletionhelper/DeletionHelperActivity.java
@@ -25,7 +25,6 @@
 import android.text.Spanned;
 import android.text.TextPaint;
 import android.text.style.ClickableSpan;
-import android.util.DisplayMetrics;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
@@ -36,6 +35,7 @@
 import com.android.settingslib.widget.LinkTextView;
 import com.android.storagemanager.ButtonBarProvider;
 import com.android.storagemanager.R;
+import com.android.storagemanager.utils.Utils;
 
 /**
  * The DeletionHelperActivity is an activity for deleting apps, photos, and downloaded files which
@@ -86,9 +86,6 @@
     void setIsEmptyState(boolean isEmptyState) {
         final View emptyContent = findViewById(R.id.empty_state);
         final View mainContent = findViewById(R.id.main_content);
-        // Check if we need to animate now since we will modify visibility before the animation
-        // starts
-        final boolean shouldAnimate = isEmptyState && emptyContent.getVisibility() != View.VISIBLE;
 
         // Update UI
         mainContent.setVisibility(isEmptyState ? View.GONE : View.VISIBLE);
@@ -99,30 +96,22 @@
         // We are giving the user the option to show all in the interstitial, so let's hide the
         // overflow for this. (Also, the overflow's functions are busted while the empty view is
         // showing, so this also works around this bug.)
-        mIsShowingInterstitial = shouldAnimate;
+        mIsShowingInterstitial = isEmptyState && emptyContent.getVisibility() != View.VISIBLE;
         invalidateOptionsMenu();
-
-        // Animate UI changes
-        if (!shouldAnimate) {
-            return;
-        }
-        animateToEmptyState();
     }
 
-    private void animateToEmptyState() {
-        View content = findViewById(R.id.empty_state);
+    public boolean isLoadingVisible() {
+        View loading_container = findViewById(R.id.loading_container);
+        if (loading_container != null) {
+            return loading_container.getVisibility() == View.VISIBLE;
+        }
+        return false;
+    }
 
-        // Animate the empty state in
-        DisplayMetrics displayMetrics = new DisplayMetrics();
-        getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
-        final float oldX = content.getTranslationX();
-        content.setTranslationX(oldX + displayMetrics.widthPixels);
-        content.animate()
-                .translationX(oldX)
-                .withEndAction(
-                        () -> {
-                            content.setTranslationX(oldX);
-                        });
+    public void setLoading(View listView, boolean loading, boolean animate) {
+        View loading_container = findViewById(R.id.loading_container);
+        Utils.handleLoadingContainer(loading_container, listView, !loading, animate);
+        getButtonBar().setVisibility(loading ? View.GONE : View.VISIBLE);
     }
 
     @Override
diff --git a/src/com/android/storagemanager/deletionhelper/DeletionHelperSettings.java b/src/com/android/storagemanager/deletionhelper/DeletionHelperSettings.java
index 2984df4..7185caa 100644
--- a/src/com/android/storagemanager/deletionhelper/DeletionHelperSettings.java
+++ b/src/com/android/storagemanager/deletionhelper/DeletionHelperSettings.java
@@ -27,9 +27,11 @@
 import android.support.v7.preference.Preference;
 import android.support.v7.preference.PreferenceScreen;
 import android.text.format.Formatter;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.Button;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -73,6 +75,7 @@
     private Button mCancel, mFree;
     private DeletionHelperFeatureProvider mProvider;
     private int mThresholdType;
+    private LoadingSpinnerController mLoadingController;
 
     public static DeletionHelperSettings newInstance(int thresholdType) {
         DeletionHelperSettings instance = new DeletionHelperSettings();
@@ -89,6 +92,7 @@
         mApps = (AppDeletionPreferenceGroup) findPreference(APPS_KEY);
         mPhotoPreference = (PhotosDeletionPreference) findPreference(KEY_PHOTOS_VIDEOS_PREFERENCE);
         mProvider = FeatureFactory.getFactory(getActivity()).getDeletionHelperFeatureProvider();
+        mLoadingController = new LoadingSpinnerController((DeletionHelperActivity) getActivity());
         if (mProvider != null) {
             mPhotoVideoDeletion =
                     mProvider.createPhotoVideoDeletionType(getContext(), mThresholdType);
@@ -200,6 +204,9 @@
     @Override
     public void onResume() {
         super.onResume();
+
+        mLoadingController.initializeLoading(getListView());
+
         for (int i = 0, size = mDeletableContentList.size(); i < size; i++) {
             mDeletableContentList.get(i).onResume();
         }
@@ -229,6 +236,10 @@
 
     @Override
     public void onFreeableChanged(int numItems, long bytesFreeable) {
+        if (bytesFreeable > 0 || allTypesEmpty()) {
+            mLoadingController.onCategoryLoad();
+        }
+
         // bytesFreeable is the number of bytes freed by a single deletion type. If it is non-zero,
         // there is stuff to free and we can enable it. If it is zero, though, we still need to get
         // getTotalFreeableSpace to check all deletion types.
@@ -243,7 +254,6 @@
     }
 
     private boolean allTypesEmpty() {
-
         return mAppBackend.isEmpty()
                 && mDownloadsDeletion.isEmpty()
                 && (mPhotoVideoDeletion == null || mPhotoVideoDeletion.isEmpty());
@@ -306,6 +316,13 @@
         }
     }
 
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        View view = super.onCreateView(inflater, container, savedInstanceState);
+        return view;
+    }
+
     private void initializeButtons() {
         ButtonBarProvider activity = (ButtonBarProvider) getActivity();
         activity.getButtonBar().setVisibility(View.VISIBLE);
@@ -344,4 +361,5 @@
         }
         return freeableSpace;
     }
+
 }
diff --git a/src/com/android/storagemanager/deletionhelper/LoadingSpinnerController.java b/src/com/android/storagemanager/deletionhelper/LoadingSpinnerController.java
new file mode 100644
index 0000000..04d3ec4
--- /dev/null
+++ b/src/com/android/storagemanager/deletionhelper/LoadingSpinnerController.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 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.storagemanager.deletionhelper;
+
+import android.view.View;
+
+/** Handles whether or not to hide/show the loading progress spinner. */
+public class LoadingSpinnerController {
+    private boolean mHasLoadedACategory;
+    private View mListView;
+    private DeletionHelperActivity mParentActivity;
+
+    /**
+     * Initializes the spinner with an activity that contains both a content view and a loading
+     * view.
+     */
+    public LoadingSpinnerController(DeletionHelperActivity activity) {
+        mParentActivity = activity;
+    }
+
+    /**
+     * Initializes the loading progress bar.
+     *
+     * @param listView A content view to potentially swap in for the loading screen.
+     */
+    public void initializeLoading(View listView) {
+        mListView = listView;
+        if (!mHasLoadedACategory) {
+            setLoading(true);
+        }
+    }
+
+    /** If a category loads, we should hide the loading progress bar. This hides the loading. */
+    public void onCategoryLoad() {
+        mHasLoadedACategory = true;
+        setLoading(false);
+    }
+
+    private void setLoading(boolean isLoading) {
+        if (mListView != null && mParentActivity.isLoadingVisible() != isLoading) {
+            mParentActivity.setLoading(mListView, isLoading, true);
+        }
+    }
+}
diff --git a/src/com/android/storagemanager/utils/Utils.java b/src/com/android/storagemanager/utils/Utils.java
new file mode 100644
index 0000000..0c5484c
--- /dev/null
+++ b/src/com/android/storagemanager/utils/Utils.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 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.storagemanager.utils;
+
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.AnimationUtils;
+
+public class Utils {
+
+    public static void handleLoadingContainer(
+            View loading, View doneLoading, boolean done, boolean animate) {
+        setViewShown(loading, !done, animate);
+        setViewShown(doneLoading, done, animate);
+    }
+
+    private static void setViewShown(final View view, boolean shown, boolean animate) {
+        if (animate) {
+            Animation animation =
+                    AnimationUtils.loadAnimation(
+                            view.getContext(),
+                            shown ? android.R.anim.fade_in : android.R.anim.fade_out);
+            if (shown) {
+                view.setVisibility(View.VISIBLE);
+            } else {
+                animation.setAnimationListener(
+                        new AnimationListener() {
+                            @Override
+                            public void onAnimationStart(Animation animation) {}
+
+                            @Override
+                            public void onAnimationRepeat(Animation animation) {}
+
+                            @Override
+                            public void onAnimationEnd(Animation animation) {
+                                view.setVisibility(View.GONE);
+                            }
+                        });
+            }
+            view.startAnimation(animation);
+        } else {
+            view.clearAnimation();
+            view.setVisibility(shown ? View.VISIBLE : View.GONE);
+        }
+    }
+}