Fix bottom sheet race condition and well handle drag stitutation

- Add a custom BottomSheetBehavior to perform a queue of state to fix race condition.
- Also well handle drag stitutation

Video: https://drive.google.com/file/d/1jsBsyccmBNZ6bixMnBfMQOy66Rc3dzHf/view?usp=sharing

Test: Manually
Bug: 158558935
Change-Id: Ia62720a40e2beb735b75dfeaad1d7540c2a4a4a4
diff --git a/res/layout/bottom_actions_layout.xml b/res/layout/bottom_actions_layout.xml
index c752303..31ad2cc 100644
--- a/res/layout/bottom_actions_layout.xml
+++ b/res/layout/bottom_actions_layout.xml
@@ -35,7 +35,7 @@
             android:elevation="@dimen/bottom_action_bar_elevation"
             android:layout_marginTop="@dimen/bottom_action_bar_bottom_sheet_margin_top"
             app:behavior_peekHeight="@dimen/preview_attribution_pane_collapsed_height"
-            app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"/>
+            app:layout_behavior="com.android.wallpaper.widget.BottomActionBar$QueueStateBottomSheetBehavior"/>
     </androidx.coordinatorlayout.widget.CoordinatorLayout>
 
     <!--  Bottom Tabs  -->
diff --git a/src/com/android/wallpaper/picker/LivePreviewFragment.java b/src/com/android/wallpaper/picker/LivePreviewFragment.java
index 29a59f9..efc4d93 100644
--- a/src/com/android/wallpaper/picker/LivePreviewFragment.java
+++ b/src/com/android/wallpaper/picker/LivePreviewFragment.java
@@ -257,7 +257,7 @@
             if (mWallpaperConnection != null && mWallpaperConnection.getEngine() != null) {
                 int action = ev.getActionMasked();
                 if (action == MotionEvent.ACTION_DOWN) {
-                    mBottomActionBar.expandBottomSheet(false);
+                    mBottomActionBar.collapseBottomSheetIfExpanded();
                 }
                 MotionEvent dup = MotionEvent.obtainNoHistory(ev);
                 try {
diff --git a/src/com/android/wallpaper/widget/BottomActionBar.java b/src/com/android/wallpaper/widget/BottomActionBar.java
index cd779a9..a8d921b 100644
--- a/src/com/android/wallpaper/widget/BottomActionBar.java
+++ b/src/com/android/wallpaper/widget/BottomActionBar.java
@@ -35,7 +35,9 @@
 import com.google.android.material.bottomsheet.BottomSheetBehavior;
 import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback;
 
+import java.util.ArrayDeque;
 import java.util.Arrays;
+import java.util.Deque;
 import java.util.EnumMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -77,21 +79,7 @@
     // TODO(b/154299462): Separate downloadable related actions from WallpaperPicker.
     /** The action items in the bottom action bar. */
     public enum BottomAction {
-        ROTATION, DELETE, INFORMATION(true), EDIT, CUSTOMIZE(true), DOWNLOAD, PROGRESS, APPLY;
-
-        private final boolean mExpandable;
-
-        BottomAction() {
-            this(/* expandable= */ false);
-        }
-
-        BottomAction(boolean expandable) {
-            mExpandable = expandable;
-        }
-
-        public boolean isExpandable() {
-            return mExpandable;
-        }
+        ROTATION, DELETE, INFORMATION, EDIT, CUSTOMIZE, DOWNLOAD, PROGRESS, APPLY
     }
 
     private final Map<BottomAction, View> mActionMap = new EnumMap<>(BottomAction.class);
@@ -100,12 +88,9 @@
             new EnumMap<>(BottomAction.class);
 
     private final ViewGroup mBottomSheetView;
-    private final BottomSheetBehavior<ViewGroup> mBottomSheetBehavior;
+    private final QueueStateBottomSheetBehavior<ViewGroup> mBottomSheetBehavior;
     private final Set<VisibilityChangeListener> mVisibilityChangeListeners = new HashSet<>();
 
-    // Track the current bottom sheet action which the content of the bottom sheet view
-    // corresponds to
-    private BottomAction mCurrentBottomSheetAction;
     // The current selected action in the BottomActionBar, can be null when no action is selected.
     private BottomAction mSelectedAction;
 
@@ -125,19 +110,36 @@
         mBottomSheetView = findViewById(R.id.action_bottom_sheet);
         SizeCalculator.adjustBackgroundCornerRadius(mBottomSheetView);
 
-        mBottomSheetBehavior = BottomSheetBehavior.from(mBottomSheetView);
+        mBottomSheetBehavior = (QueueStateBottomSheetBehavior<ViewGroup>) BottomSheetBehavior.from(
+                mBottomSheetView);
         mBottomSheetBehavior.setState(STATE_COLLAPSED);
         mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetCallback() {
             @Override
             public void onStateChanged(@NonNull View bottomSheet, int newState) {
-                if (mCurrentBottomSheetAction == null) {
+                if (mBottomSheetBehavior.isQueueProcessing()) {
+                    // Avoid button and bottom sheet mismatching from quick tapping buttons when
+                    // bottom sheet is changing state.
+                    disableActions();
+                    // If bottom sheet is going with expanded-collapsed-expanded, the new content
+                    // will be updated in collapsed state. The first state change from expanded to
+                    // collapsed should still show the previous content view.
+                    if (mSelectedAction != null && newState == STATE_COLLAPSED) {
+                        updateContentViewFor(mSelectedAction);
+                    }
                     return;
                 }
 
+                // Enable all buttons when queue is not processing.
+                enableActions();
+                if (mSelectedAction == null) {
+                    return;
+                }
+                // Ensure the button state should be the same as bottom sheet state to catch up the
+                // state change from dragging or some unexpected bottom sheet state changes.
                 if (newState == STATE_COLLAPSED) {
-                    updateSelectedState(mCurrentBottomSheetAction, /* selected= */ false);
+                    updateSelectedState(mSelectedAction, /* selected= */ false);
                 } else if (newState == STATE_EXPANDED) {
-                    updateSelectedState(mCurrentBottomSheetAction, /* selected= */ true);
+                    updateSelectedState(mSelectedAction, /* selected= */ true);
                 }
             }
             @Override
@@ -155,7 +157,8 @@
     public void onVisibilityAggregated(boolean isVisible) {
         super.onVisibilityAggregated(isVisible);
         if (!isVisible) {
-            mBottomSheetBehavior.setState(STATE_COLLAPSED);
+            hideBottomSheetAndDeselectButtonIfExpanded();
+            mBottomSheetBehavior.reset();
         }
         mVisibilityChangeListeners.forEach(listener -> listener.onVisibilityChange(isVisible));
     }
@@ -173,19 +176,16 @@
         mContentViewMap.put(action, contentView);
         mBottomSheetView.addView(contentView);
         setActionClickListener(action, actionView -> {
-            mCurrentBottomSheetAction = action;
-            mContentViewMap.forEach((a, v) -> v.setVisibility(a.equals(action) ? VISIBLE : GONE));
+            if (mBottomSheetBehavior.getState() == STATE_COLLAPSED) {
+                updateContentViewFor(action);
+            }
             mBottomSheetView.setAccessibilityTraversalAfter(actionView.getId());
         });
     }
 
-    /**
-     * Expands or collapses bottom sheet.
-     *
-     * @param isExpand {@code true} if bottom sheet is to be expanded; {@code false} otherwise.
-     */
-    public void expandBottomSheet(boolean isExpand) {
-        mBottomSheetBehavior.setState(isExpand ? STATE_EXPANDED : STATE_COLLAPSED);
+    /** Collapses the bottom sheet. */
+    public void collapseBottomSheetIfExpanded() {
+        hideBottomSheetAndDeselectButtonIfExpanded();
     }
 
     /**
@@ -202,11 +202,14 @@
                     "Had already set a click listener to button: " + bottomAction);
         }
         buttonView.setOnClickListener(view -> {
-            if (mSelectedAction != null) {
+            if (mSelectedAction != null && isActionSelected(mSelectedAction)) {
                 updateSelectedState(mSelectedAction, /* selected= */ false);
-                if (mSelectedAction.isExpandable()) {
-                    mBottomSheetBehavior.setState(STATE_COLLAPSED);
+                if (isExpandable(mSelectedAction)) {
+                    mBottomSheetBehavior.enqueue(STATE_COLLAPSED);
                 }
+            } else {
+                // Error handling, set to null if the action is not selected.
+                mSelectedAction = null;
             }
 
             if (bottomAction == mSelectedAction) {
@@ -216,11 +219,12 @@
                 // Select a different action from the current selected action.
                 mSelectedAction = bottomAction;
                 updateSelectedState(mSelectedAction, /* selected= */ true);
-                if (mSelectedAction.isExpandable()) {
-                    mBottomSheetBehavior.setState(STATE_EXPANDED);
+                if (isExpandable(mSelectedAction)) {
+                    mBottomSheetBehavior.enqueue(STATE_EXPANDED);
                 }
             }
             actionClickListener.onClick(view);
+            mBottomSheetBehavior.processQueueForStateChange();
         });
     }
 
@@ -293,8 +297,8 @@
         for (BottomAction action : actions) {
             mActionMap.get(action).setVisibility(GONE);
 
-            if (action.isExpandable() && mSelectedAction == action) {
-                mBottomSheetBehavior.setState(STATE_COLLAPSED);
+            if (isExpandable(action) && mSelectedAction == action) {
+                hideBottomSheetAndDeselectButtonIfExpanded();
             }
         }
     }
@@ -383,10 +387,11 @@
         hideAllActions();
         clearActionClickListeners();
         enableActions();
-        mActionMap.forEach(
-                (action, view) -> updateSelectedState(action, /* selected= */ false));
+        mActionMap.keySet().forEach(a -> updateSelectedState(a, /* selected= */ false));
         mBottomSheetView.removeAllViews();
         mContentViewMap.clear();
+        mBottomSheetBehavior.reset();
+        mSelectedAction = null;
     }
 
     /** Clears all the actions' click listeners */
@@ -407,4 +412,97 @@
         }
         bottomActionView.setSelected(selected);
     }
+
+    private void hideBottomSheetAndDeselectButtonIfExpanded() {
+        if (isExpandable(mSelectedAction) && mBottomSheetBehavior.getState() == STATE_EXPANDED) {
+            mBottomSheetBehavior.setState(STATE_COLLAPSED);
+            updateSelectedState(mSelectedAction, /* selected= */ false);
+            mSelectedAction = null;
+        }
+    }
+
+    private void updateContentViewFor(BottomAction action) {
+        mContentViewMap.forEach((a, v) -> v.setVisibility(a.equals(action) ? VISIBLE : GONE));
+    }
+
+    private boolean isExpandable(BottomAction action) {
+        return action != null && mContentViewMap.containsKey(action);
+    }
+
+    /** A {@link BottomSheetBehavior} that can process a queue of bottom sheet states.*/
+    public static class QueueStateBottomSheetBehavior<V extends View>
+            extends BottomSheetBehavior<V> {
+
+        private final Deque<Integer> mStateQueue = new ArrayDeque<>();
+        private boolean mIsQueueProcessing;
+
+        public QueueStateBottomSheetBehavior(Context context, @Nullable AttributeSet attrs) {
+            super(context, attrs);
+            // Binds the default callback for processing queue.
+            setBottomSheetCallback(null);
+        }
+
+        /** Enqueues the bottom sheet states. */
+        public void enqueue(int state) {
+            if (!mStateQueue.isEmpty() && mStateQueue.getLast() == state) {
+                return;
+            }
+            mStateQueue.add(state);
+        }
+
+        /** Processes the queue of bottom sheet state that was set via {@link #enqueue}. */
+        public void processQueueForStateChange() {
+            if (mStateQueue.isEmpty()) {
+                return;
+            }
+            setState(mStateQueue.getFirst());
+            mIsQueueProcessing = true;
+        }
+
+        /**
+         * Returns {@code true} if the queue is processing. For example, if the bottom sheet is
+         * going with expanded-collapsed-expanded, it would return {@code true} until last expanded
+         * state is finished. */
+        public boolean isQueueProcessing() {
+            return mIsQueueProcessing;
+        }
+
+        /** Resets the queue state. */
+        public void reset() {
+            mStateQueue.clear();
+            mIsQueueProcessing = false;
+        }
+
+        @Override
+        public void setBottomSheetCallback(BottomSheetCallback callback) {
+            super.setBottomSheetCallback(new BottomSheetCallback() {
+                @Override
+                public void onStateChanged(@NonNull View bottomSheet, int newState) {
+                    if (!mStateQueue.isEmpty()) {
+                        if (newState == mStateQueue.getFirst()) {
+                            mStateQueue.removeFirst();
+                            if (mStateQueue.isEmpty()) {
+                                mIsQueueProcessing = false;
+                            } else {
+                                setState(mStateQueue.getFirst());
+                            }
+                        } else {
+                            setState(mStateQueue.getFirst());
+                        }
+                    }
+
+                    if (callback != null) {
+                        callback.onStateChanged(bottomSheet, newState);
+                    }
+                }
+
+                @Override
+                public void onSlide(@NonNull View bottomSheet, float slideOffset) {
+                    if (callback != null) {
+                        callback.onSlide(bottomSheet, slideOffset);
+                    }
+                }
+            });
+        }
+    }
 }