Add ActionBarController

* Collect all actionBar interactions within DialtactsActivity into
a single controller to ensure that it behaves more deterministically,
and fix some bugs with regards to actionBar interactions.

* Make sure that action bar correctly handles activity recreation
and destruction by saving its state

* Add unit tests and mock classes for ActionBarController

Bug: 14900155

Change-Id: I370831db425e1970b118f5d4fed3ce9297e3610d
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index 8445ff7..d8a02ac 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -20,7 +20,6 @@
 import android.app.ActionBar;
 import android.app.Activity;
 import android.app.Fragment;
-import android.app.FragmentManager;
 import android.app.FragmentTransaction;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
@@ -85,6 +84,7 @@
 import com.android.dialer.list.RemoveView;
 import com.android.dialer.list.SearchFragment;
 import com.android.dialer.list.SmartDialSearchFragment;
+import com.android.dialer.widget.ActionBarController;
 import com.android.dialer.widget.SearchEditTextLayout;
 import com.android.dialer.widget.SearchEditTextLayout.OnBackButtonClickedListener;
 import com.android.dialerbind.DatabaseHelperManager;
@@ -102,10 +102,12 @@
         DialpadFragment.HostInterface,
         ListsFragment.HostInterface,
         SpeedDialFragment.HostInterface,
+        SearchFragment.HostInterface,
         OnDragDropListener,
         OnPhoneNumberPickerActionListener,
         PopupMenu.OnMenuItemClickListener,
-        ViewPager.OnPageChangeListener {
+        ViewPager.OnPageChangeListener,
+        ActionBarController.ActivityUi {
     private static final String TAG = "DialtactsActivity";
 
     public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -208,6 +210,7 @@
 
     private DialerDatabaseHelper mDialerDatabaseHelper;
     private DragDropController mDragDropController;
+    private ActionBarController mActionBarController;
 
     private class OptionsPopupMenu extends PopupMenu {
         public OptionsPopupMenu(Context context, View anchor) {
@@ -293,6 +296,7 @@
         @Override
         public void onClick(View v) {
             if (!isInSearchUi()) {
+                mActionBarController.onSearchBoxTapped();
                 enterSearchUi(false /* smartDialSearch */, mSearchView.getText().toString());
             }
         }
@@ -325,6 +329,9 @@
         actionBar.setDisplayShowCustomEnabled(true);
         actionBar.setBackgroundDrawable(null);
 
+        mActionBarController = new ActionBarController(this,
+                (SearchEditTextLayout) actionBar.getCustomView());
+
         mSearchEditTextLayout = (SearchEditTextLayout) actionBar.getCustomView();
         mSearchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener);
 
@@ -363,6 +370,7 @@
             mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI);
             mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI);
             mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH);
+            mActionBarController.restoreInstanceState(savedInstanceState);
         }
 
         parentLayout = (RelativeLayout) findViewById(R.id.dialtacts_mainlayout);
@@ -426,6 +434,7 @@
         outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
         outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch);
         outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
+        mActionBarController.saveInstanceState(outState);
     }
 
     @Override
@@ -563,6 +572,8 @@
         ft.show(mDialpadFragment);
         ft.commit();
 
+        mActionBarController.onDialpadUp();
+
         if (!isInSearchUi()) {
             enterSearchUi(true /* isSmartDial */, mSearchQuery);
         }
@@ -581,7 +592,6 @@
         }
 
         updateSearchFragmentPosition();
-        getActionBar().hide();
     }
 
     /**
@@ -617,7 +627,7 @@
             commitDialpadFragmentHide();
         }
 
-        mListsFragment.maybeShowActionBar();
+        mActionBarController.onDialpadDown();
 
         if (isInSearchUi()) {
             if (TextUtils.isEmpty(mSearchQuery)) {
@@ -649,10 +659,21 @@
         }
     }
 
-    private boolean isInSearchUi() {
+    @Override
+    public boolean isInSearchUi() {
         return mInDialpadSearch || mInRegularSearch;
     }
 
+    @Override
+    public boolean hasSearchQuery() {
+        return !TextUtils.isEmpty(mSearchQuery);
+    }
+
+    @Override
+    public boolean shouldShowActionBar() {
+        return mListsFragment.shouldShowActionBar();
+    }
+
     private void setNotInSearchUi() {
         mInDialpadSearch = false;
         mInRegularSearch = false;
@@ -829,7 +850,6 @@
         transaction.commit();
 
         mListsFragment.getView().animate().alpha(0).withLayer();
-        mSearchEditTextLayout.animateExpandOrCollapse(true);
     }
 
     /**
@@ -856,7 +876,7 @@
         transaction.commit();
 
         mListsFragment.getView().animate().alpha(1).withLayer();
-        mSearchEditTextLayout.animateExpandOrCollapse(false);
+        mActionBarController.onSearchUiExited();
     }
 
     /** Returns an Intent to launch Call Settings screen */
@@ -974,7 +994,7 @@
      */
     @Override
     public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) {
-        getActionBar().hide();
+        mActionBarController.slideActionBarUp(true);
         mRemoveViewContainer.setVisibility(View.VISIBLE);
     }
 
@@ -987,7 +1007,7 @@
      */
     @Override
     public void onDragFinished(int x, int y) {
-        getActionBar().show();
+        mActionBarController.slideActionBarDown(true);
         mRemoveViewContainer.setVisibility(View.GONE);
     }
 
@@ -1031,10 +1051,6 @@
         exitSearchUi();
     }
 
-    public int getActionBarHeight() {
-        return mActionBarHeight;
-    }
-
     @Override
     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
 
@@ -1107,4 +1123,24 @@
         public void onAnimationRepeat(Animation animation) {
         }
     }
+
+    @Override
+    public boolean isActionBarShowing() {
+        return mActionBarController.isActionBarShowing();
+    }
+
+    @Override
+    public int getActionBarHideOffset() {
+        return getActionBar().getHideOffset();
+    }
+
+    @Override
+    public int getActionBarHeight() {
+        return mActionBarHeight;
+    }
+
+    @Override
+    public void setActionBarHideOffset(int hideOffset) {
+        getActionBar().setHideOffset(hideOffset);
+    }
 }
diff --git a/src/com/android/dialer/list/ListsFragment.java b/src/com/android/dialer/list/ListsFragment.java
index 78570e1..2ff5a2a 100644
--- a/src/com/android/dialer/list/ListsFragment.java
+++ b/src/com/android/dialer/list/ListsFragment.java
@@ -326,13 +326,8 @@
         }
     }
 
-    public void maybeShowActionBar() {
-        // TODO: Try to show the action bar regardless of whether the panel is open, and then update
-        // the offset to show/hide the action bar, instead of updating the whether the action bar is
-        // shown in onPanelSlide.
-        if (mIsPanelOpen && mActionBar != null) {
-            mActionBar.show();
-        }
+    public boolean shouldShowActionBar() {
+        return mIsPanelOpen && mActionBar != null;
     }
 
     private void setupPaneLayout(OverlappingPaneLayout paneLayout) {
diff --git a/src/com/android/dialer/list/SearchFragment.java b/src/com/android/dialer/list/SearchFragment.java
index f863d90..9a30c4d 100644
--- a/src/com/android/dialer/list/SearchFragment.java
+++ b/src/com/android/dialer/list/SearchFragment.java
@@ -45,6 +45,12 @@
     private String mAddToContactNumber;
     private int mActionBarHeight;
 
+    public interface HostInterface {
+        public boolean isActionBarShowing();
+        public int getActionBarHideOffset();
+        public int getActionBarHeight();
+    }
+
     @Override
     public void onAttach(Activity activity) {
         super.onAttach(activity);
@@ -69,7 +75,9 @@
             getAdapter().setHasHeader(0, false);
         }
 
-        mActionBarHeight = ((DialtactsActivity) getActivity()).getActionBarHeight();
+        HostInterface activity = (HostInterface) getActivity();
+
+        mActionBarHeight = activity.getActionBarHeight();
 
         final View parentView = getView();
         parentView.setPaddingRelative(
@@ -92,7 +100,8 @@
             }
         });
 
-        if (!getActivity().getActionBar().isShowing()) {
+
+        if (!activity.isActionBarShowing()) {
             parentView.setTranslationY(-mActionBarHeight);
         }
     }
diff --git a/src/com/android/dialer/widget/ActionBarController.java b/src/com/android/dialer/widget/ActionBarController.java
new file mode 100644
index 0000000..49506f4
--- /dev/null
+++ b/src/com/android/dialer/widget/ActionBarController.java
@@ -0,0 +1,219 @@
+package com.android.dialer.widget;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.dialer.DialtactsActivity;
+
+/**
+ * Controls the various animated properties of the actionBar: showing/hiding, fading/revealing,
+ * and collapsing/expanding, and assigns suitable properties to the actionBar based on the
+ * current state of the UI.
+ */
+public class ActionBarController {
+    public static final boolean DEBUG = DialtactsActivity.DEBUG;
+    public static final String TAG = "ActionBarController";
+    private static final String KEY_IS_SLID_UP = "key_actionbar_is_slid_up";
+    private static final String KEY_IS_FADED_OUT = "key_actionbar_is_faded_out";
+    private static final String KEY_IS_EXPANDED = "key_actionbar_is_expanded";
+
+    private ActivityUi mActivityUi;
+    private SearchEditTextLayout mSearchBox;
+
+    private boolean mIsActionBarSlidUp;
+
+    public interface ActivityUi {
+        public boolean isInSearchUi();
+        public boolean hasSearchQuery();
+        public boolean shouldShowActionBar();
+        public int getActionBarHeight();
+        public int getActionBarHideOffset();
+        public void setActionBarHideOffset(int hideOffset);
+    }
+
+    public ActionBarController(ActivityUi activityUi, SearchEditTextLayout searchBox) {
+        mActivityUi = activityUi;
+        mSearchBox = searchBox;
+    }
+
+    /**
+     * @return The offset the action bar is being translated upwards by
+     */
+    public int getHideOffset() {
+        return mActivityUi.getActionBarHideOffset();
+    }
+
+    /**
+     * @return Whether or not the action bar is currently showing (both slid down and visible)
+     */
+    public boolean isActionBarShowing() {
+        return !mIsActionBarSlidUp && !mSearchBox.isFadedOut();
+    }
+
+    /**
+     * Called when the user has tapped on the collapsed search box, to start a new search query.
+     */
+    public void onSearchBoxTapped() {
+        if (DEBUG) {
+            Log.d(TAG, "OnSearchBoxTapped: isInSearchUi " + mActivityUi.isInSearchUi());
+        }
+        if (!mActivityUi.isInSearchUi()) {
+            mSearchBox.expand(true /* animate */, true /* requestFocus */);
+        }
+    }
+
+    /**
+     * Called when search UI has been exited for some reason.
+     */
+    public void onSearchUiExited() {
+        if (DEBUG) {
+            Log.d(TAG, "OnSearchUIExited: isExpanded " + mSearchBox.isExpanded()
+                    + " isFadedOut: " + mSearchBox.isFadedOut()
+                    + " shouldShowActionBar: " + mActivityUi.shouldShowActionBar());
+        }
+        if (mSearchBox.isExpanded()) {
+            mSearchBox.collapse(true /* animate */);
+        }
+        if (mSearchBox.isFadedOut()) {
+            mSearchBox.fadeIn();
+        }
+
+        if (mActivityUi.shouldShowActionBar()) {
+            slideActionBarDown(false /* animate */);
+        } else {
+            slideActionBarUp(false /* animate */);
+        }
+    }
+
+    /**
+     * Called to indicate that the user is trying to hide the dialpad. Should be called before
+     * any state changes have actually occurred.
+     */
+    public void onDialpadDown() {
+        if (DEBUG) {
+            Log.d(TAG, "OnDialpadDown: isInSearchUi " + mActivityUi.isInSearchUi()
+                    + " hasSearchQuery: " + mActivityUi.hasSearchQuery()
+                    + " isFadedOut: " + mSearchBox.isFadedOut()
+                    + " isExpanded: " + mSearchBox.isExpanded());
+        }
+        if (mActivityUi.isInSearchUi()) {
+            if (mActivityUi.hasSearchQuery()) {
+                if (mSearchBox.isFadedOut()) {
+                    mSearchBox.setVisible(true);
+                }
+                if (!mSearchBox.isExpanded()) {
+                    mSearchBox.expand(false /* animate */, false /* requestFocus */);
+                }
+                slideActionBarDown(true /* animate */);
+            } else {
+                mSearchBox.fadeIn();
+            }
+        }
+    }
+
+    /**
+     * Called to indicate that the user is trying to show the dialpad. Should be called before
+     * any state changes have actually occurred.
+     */
+    public void onDialpadUp() {
+        if (DEBUG) {
+            Log.d(TAG, "OnDialpadUp: isInSearchUi " + mActivityUi.isInSearchUi());
+        }
+        if (mActivityUi.isInSearchUi()) {
+            slideActionBarUp(true);
+        } else {
+            // From the lists fragment
+            mSearchBox.fadeOut();
+        }
+    }
+
+    public void slideActionBarUp(boolean animate) {
+        if (DEBUG) {
+            Log.d(TAG, "Sliding actionBar up - animate: " + animate);
+        }
+        if (animate) {
+            ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+            animator.addUpdateListener(new AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    final float value = (float) animation.getAnimatedValue();
+                    mActivityUi.setActionBarHideOffset(
+                            (int) (mActivityUi.getActionBarHeight() * value));
+                }
+            });
+            animator.start();
+        } else {
+           mActivityUi.setActionBarHideOffset(mActivityUi.getActionBarHeight());
+        }
+        mIsActionBarSlidUp = true;
+    }
+
+    public void slideActionBarDown(boolean animate) {
+        if (DEBUG) {
+            Log.d(TAG, "Sliding actionBar down - animate: " + animate);
+        }
+        if (animate) {
+            ValueAnimator animator = ValueAnimator.ofFloat(1, 0);
+            animator.addUpdateListener(new AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    final float value = (float) animation.getAnimatedValue();
+                    mActivityUi.setActionBarHideOffset(
+                            (int) (mActivityUi.getActionBarHeight() * value));
+                }
+            });
+            animator.start();
+        } else {
+            mActivityUi.setActionBarHideOffset(0);
+        }
+        mIsActionBarSlidUp = false;
+    }
+
+    /**
+     * Saves the current state of the action bar into a provided {@link Bundle}
+     */
+    public void saveInstanceState(Bundle outState) {
+        outState.putBoolean(KEY_IS_SLID_UP, mIsActionBarSlidUp);
+        outState.putBoolean(KEY_IS_FADED_OUT, mSearchBox.isFadedOut());
+        outState.putBoolean(KEY_IS_EXPANDED, mSearchBox.isExpanded());
+    }
+
+    /**
+     * Restores the action bar state from a provided {@link Bundle}
+     */
+    public void restoreInstanceState(Bundle inState) {
+        mIsActionBarSlidUp = inState.getBoolean(KEY_IS_SLID_UP);
+        if (mIsActionBarSlidUp) {
+            slideActionBarUp(false);
+        } else {
+            slideActionBarDown(false);
+        }
+
+        final boolean isSearchBoxFadedOut = inState.getBoolean(KEY_IS_FADED_OUT);
+        if (isSearchBoxFadedOut) {
+            if (!mSearchBox.isFadedOut()) {
+                mSearchBox.setVisible(false);
+            }
+        } else if (mSearchBox.isFadedOut()) {
+                mSearchBox.setVisible(true);
+        }
+
+        final boolean isSearchBoxExpanded = inState.getBoolean(KEY_IS_EXPANDED);
+        if (isSearchBoxExpanded) {
+            if (!mSearchBox.isExpanded()) {
+                mSearchBox.expand(false, false);
+            }
+        } else if (mSearchBox.isExpanded()) {
+                mSearchBox.collapse(false);
+        }
+    }
+
+    @VisibleForTesting
+    public boolean getIsActionBarSlidUp() {
+        return mIsActionBarSlidUp;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/dialer/widget/SearchEditTextLayout.java b/src/com/android/dialer/widget/SearchEditTextLayout.java
index 6830842..ef3ddcc 100644
--- a/src/com/android/dialer/widget/SearchEditTextLayout.java
+++ b/src/com/android/dialer/widget/SearchEditTextLayout.java
@@ -16,6 +16,8 @@
 
 package com.android.dialer.widget;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.content.Context;
@@ -27,7 +29,7 @@
 import android.widget.EditText;
 import android.widget.FrameLayout;
 
-import com.android.contacts.common.animation.AnimationUtils;
+import com.android.contacts.common.animation.AnimUtils;
 import com.android.dialer.R;
 
 public class SearchEditTextLayout extends FrameLayout {
@@ -39,12 +41,16 @@
     private int mLeftMargin;
     private int mRightMargin;
 
-    private int mBackgroundColor;
+    /* Subclass-visible for testing */
+    protected boolean mIsExpanded = false;
+    protected boolean mIsFadedOut = false;
 
     private View mCollapsed;
     private View mExpanded;
     private EditText mSearchView;
 
+    private ValueAnimator mAnimator;
+
     private OnBackButtonClickedListener mOnBackButtonClickedListener;
 
     /**
@@ -56,7 +62,6 @@
 
     public SearchEditTextLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mBackgroundColor = getResources().getColor(R.color.searchbox_background_color);
     }
 
     public void setPreImeKeyListener(OnKeyListener listener) {
@@ -117,32 +122,85 @@
         return super.dispatchKeyEventPreIme(event);
     }
 
-    public void animateExpandOrCollapse(boolean expand) {
-        final ValueAnimator animator;
-        if (expand) {
-            AnimationUtils.crossFadeViews(mExpanded, mCollapsed, ANIMATION_DURATION);
-            animator = ValueAnimator.ofFloat(1f, 0f);
-            setBackgroundResource(R.drawable.search_shadow);
-            mSearchView.requestFocus();
+    public void fadeOut() {
+        AnimUtils.fadeOut(this, ANIMATION_DURATION);
+        mIsFadedOut = true;
+    }
+
+    public void fadeIn() {
+        AnimUtils.fadeIn(this, ANIMATION_DURATION);
+        mIsFadedOut = false;
+    }
+
+    public void setVisible(boolean visible) {
+        if (visible) {
+            setAlpha(1);
+            setVisibility(View.VISIBLE);
+            mIsFadedOut = false;
         } else {
-            AnimationUtils.crossFadeViews(mCollapsed, mExpanded, ANIMATION_DURATION);
-            animator = ValueAnimator.ofFloat(0f, 1f);
-            setBackgroundResource(R.drawable.rounded_corner);
+            setAlpha(0);
+            setVisibility(View.GONE);
+            mIsFadedOut = true;
         }
-        animator.addUpdateListener(new AnimatorUpdateListener() {
+    }
+    public void expand(boolean animate, boolean requestFocus) {
+        if (animate) {
+            AnimUtils.crossFadeViews(mExpanded, mCollapsed, ANIMATION_DURATION);
+            mAnimator = ValueAnimator.ofFloat(1f, 0f);
+            prepareAnimator(true);
+        } else {
+            mExpanded.setVisibility(View.VISIBLE);
+            mExpanded.setAlpha(1);
+            setMargins(0f);
+            mCollapsed.setVisibility(View.GONE);
+        }
+
+        setBackgroundResource(R.drawable.search_shadow);
+        if (requestFocus) {
+            mSearchView.requestFocus();
+        }
+        mIsExpanded = true;
+    }
+
+    public void collapse(boolean animate) {
+        if (animate) {
+            AnimUtils.crossFadeViews(mCollapsed, mExpanded, ANIMATION_DURATION);
+            mAnimator = ValueAnimator.ofFloat(0f, 1f);
+            prepareAnimator(false);
+        } else {
+            mCollapsed.setVisibility(View.VISIBLE);
+            mCollapsed.setAlpha(1);
+            setMargins(1f);
+            mExpanded.setVisibility(View.GONE);
+        }
+
+        mIsExpanded = false;
+        setBackgroundResource(R.drawable.rounded_corner);
+    }
+
+    private void prepareAnimator(final boolean expand) {
+        if (mAnimator != null) {
+            mAnimator.cancel();
+        }
+
+        mAnimator.addUpdateListener(new AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator animation) {
                 final Float fraction = (Float) animation.getAnimatedValue();
-                MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
-                params.topMargin = (int) (mTopMargin * fraction);
-                params.bottomMargin = (int) (mBottomMargin * fraction);
-                params.leftMargin = (int) (mLeftMargin * fraction);
-                params.rightMargin = (int) (mRightMargin * fraction);
-                requestLayout();
+                setMargins(fraction);
             }
         });
-        animator.setDuration(ANIMATION_DURATION);
-        animator.start();
+
+        mAnimator.setDuration(ANIMATION_DURATION);
+        mAnimator.start();
+    }
+
+    public boolean isExpanded() {
+        return mIsExpanded;
+    }
+
+    public boolean isFadedOut() {
+        return mIsFadedOut;
     }
 
     private void showInputMethod(View view) {
@@ -152,4 +210,18 @@
             imm.showSoftInput(view, 0);
         }
     }
+
+    /**
+     * Assigns margins to the search box as a fraction of its maximum margin size
+     *
+     * @param fraction How large the margins should be as a fraction of their full size
+     */
+    private void setMargins(float fraction) {
+        MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
+        params.topMargin = (int) (mTopMargin * fraction);
+        params.bottomMargin = (int) (mBottomMargin * fraction);
+        params.leftMargin = (int) (mLeftMargin * fraction);
+        params.rightMargin = (int) (mRightMargin * fraction);
+        requestLayout();
+    }
 }
\ No newline at end of file