1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.wallpaper.widget;
17 
18 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
19 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
20 
21 import android.app.Activity;
22 import android.content.Context;
23 import android.content.res.ColorStateList;
24 import android.text.TextUtils;
25 import android.util.AttributeSet;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.view.accessibility.AccessibilityEvent;
30 import android.widget.FrameLayout;
31 import android.widget.ImageView;
32 import android.widget.ProgressBar;
33 
34 import androidx.annotation.LayoutRes;
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.core.widget.ImageViewCompat;
38 
39 import com.android.internal.util.ArrayUtils;
40 import com.android.wallpaper.R;
41 import com.android.wallpaper.util.ResourceUtils;
42 import com.android.wallpaper.util.SizeCalculator;
43 
44 import com.google.android.material.bottomsheet.BottomSheetBehavior;
45 import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback;
46 
47 import java.util.ArrayDeque;
48 import java.util.Arrays;
49 import java.util.Deque;
50 import java.util.EnumMap;
51 import java.util.HashSet;
52 import java.util.Map;
53 import java.util.Set;
54 
55 /** A {@code ViewGroup} which provides the specific actions for the user to interact with. */
56 public class BottomActionBar extends FrameLayout {
57 
58     /**
59      * Interface to be implemented by an Activity hosting a {@link BottomActionBar}
60      */
61     public interface BottomActionBarHost {
62         /** Gets {@link BottomActionBar}. */
getBottomActionBar()63         BottomActionBar getBottomActionBar();
64     }
65 
66     /**
67      * The listener for {@link BottomActionBar} visibility change notification.
68      */
69     public interface VisibilityChangeListener {
70         /**
71          * Called when {@link BottomActionBar} visibility changes.
72          *
73          * @param isVisible {@code true} if it's visible; {@code false} otherwise.
74          */
onVisibilityChange(boolean isVisible)75         void onVisibilityChange(boolean isVisible);
76     }
77 
78     /** This listens to changes to an action view's selected state. */
79     public interface OnActionSelectedListener {
80 
81         /**
82          * This is called when an action view's selected state changes.
83          * @param selected whether the action view is selected.
84          */
onActionSelected(boolean selected)85         void onActionSelected(boolean selected);
86     }
87 
88     /**
89      *  A Callback to notify the registrant to change it's accessibility param when
90      *  {@link BottomActionBar} state changes.
91      */
92     public interface AccessibilityCallback {
93         /**
94          * Called when {@link BottomActionBar} collapsed.
95          */
onBottomSheetCollapsed()96         void onBottomSheetCollapsed();
97 
98         /**
99          * Called when {@link BottomActionBar} expanded.
100          */
onBottomSheetExpanded()101         void onBottomSheetExpanded();
102     }
103 
104     /**
105      * Object to host content view for bottom sheet to display.
106      *
107      * <p> The view would be created in the constructor.
108      */
109     public static abstract class BottomSheetContent<T extends View> {
110 
111         private T mContentView;
112         private boolean mIsVisible;
113 
BottomSheetContent(Context context)114         public BottomSheetContent(Context context) {
115             mContentView = createView(context);
116             setVisibility(false);
117         }
118 
119         /** Gets the view id to inflate. */
120         @LayoutRes
getViewId()121         public abstract int getViewId();
122 
123         /** Gets called when the content view is created. */
onViewCreated(T view)124         public abstract void onViewCreated(T view);
125 
126         /** Gets called when the current content view is going to recreate. */
onRecreateView(T oldView)127         public void onRecreateView(T oldView) {}
128 
recreateView(Context context)129         private void recreateView(Context context) {
130             // Inform that the view is going to recreate.
131             onRecreateView(mContentView);
132             // Create a new view with the given context.
133             mContentView = createView(context);
134             setVisibility(mIsVisible);
135         }
136 
createView(Context context)137         private T createView(Context context) {
138             T contentView = (T) LayoutInflater.from(context).inflate(getViewId(), null);
139             onViewCreated(contentView);
140             contentView.setFocusable(true);
141             return contentView;
142         }
143 
setVisibility(boolean isVisible)144         protected void setVisibility(boolean isVisible) {
145             mIsVisible = isVisible;
146             mContentView.setVisibility(mIsVisible ? VISIBLE : GONE);
147         }
148     }
149 
150     // TODO(b/154299462): Separate downloadable related actions from WallpaperPicker.
151     /** The action items in the bottom action bar. */
152     public enum BottomAction {
153         ROTATION,
154         DELETE,
155         INFORMATION(R.string.accessibility_info_shown, R.string.accessibility_info_hidden),
156         EDIT,
157         CUSTOMIZE(R.string.accessibility_customize_shown, R.string.accessibility_customize_hidden),
158         EFFECTS,
159         DOWNLOAD,
160         PROGRESS,
161         APPLY,
162         APPLY_TEXT;
163 
164         private final int mShownAccessibilityResId;
165         private final int mHiddenAccessibilityResId;
166 
BottomAction()167         BottomAction() {
168             this(/* shownAccessibilityLabelResId= */ 0, /* shownAccessibilityLabelResId= */ 0);
169         }
170 
BottomAction(int shownAccessibilityLabelResId, int hiddenAccessibilityLabelResId)171         BottomAction(int shownAccessibilityLabelResId, int hiddenAccessibilityLabelResId) {
172             mShownAccessibilityResId = shownAccessibilityLabelResId;
173             mHiddenAccessibilityResId = hiddenAccessibilityLabelResId;
174         }
175 
176         /**
177          * Returns the string resource id of the currently bottom action for its shown or hidden
178          * state.
179          */
getAccessibilityStringRes(boolean isShown)180         public int getAccessibilityStringRes(boolean isShown) {
181             return isShown ? mShownAccessibilityResId : mHiddenAccessibilityResId;
182         }
183     }
184 
185     private final Map<BottomAction, View> mActionMap = new EnumMap<>(BottomAction.class);
186     private final Map<BottomAction, BottomSheetContent<?>> mContentViewMap =
187             new EnumMap<>(BottomAction.class);
188     private final Map<BottomAction, OnActionSelectedListener> mActionSelectedListeners =
189             new EnumMap<>(BottomAction.class);
190 
191     private final ViewGroup mBottomSheetView;
192     private final QueueStateBottomSheetBehavior<ViewGroup> mBottomSheetBehavior;
193     private final Set<VisibilityChangeListener> mVisibilityChangeListeners = new HashSet<>();
194 
195     // The current selected action in the BottomActionBar, can be null when no action is selected.
196     @Nullable private BottomAction mSelectedAction;
197     // The last selected action in the BottomActionBar.
198     @Nullable private BottomAction mLastSelectedAction;
199     @Nullable private AccessibilityCallback mAccessibilityCallback;
200 
BottomActionBar(@onNull Context context, @Nullable AttributeSet attrs)201     public BottomActionBar(@NonNull Context context, @Nullable AttributeSet attrs) {
202         super(context, attrs);
203         LayoutInflater.from(context).inflate(R.layout.bottom_actions_layout, this, true);
204 
205         mActionMap.put(BottomAction.ROTATION, findViewById(R.id.action_rotation));
206         mActionMap.put(BottomAction.DELETE, findViewById(R.id.action_delete));
207         mActionMap.put(BottomAction.INFORMATION, findViewById(R.id.action_information));
208         mActionMap.put(BottomAction.EDIT, findViewById(R.id.action_edit));
209         mActionMap.put(BottomAction.CUSTOMIZE, findViewById(R.id.action_customize));
210         mActionMap.put(BottomAction.EFFECTS, findViewById(R.id.action_effects));
211         mActionMap.put(BottomAction.DOWNLOAD, findViewById(R.id.action_download));
212         mActionMap.put(BottomAction.PROGRESS, findViewById(R.id.action_progress));
213         mActionMap.put(BottomAction.APPLY, findViewById(R.id.action_apply));
214         mActionMap.put(BottomAction.APPLY_TEXT, findViewById(R.id.action_apply_text_button));
215 
216         mBottomSheetView = findViewById(R.id.action_bottom_sheet);
217         SizeCalculator.adjustBackgroundCornerRadius(mBottomSheetView);
218         setColor(context);
219 
220         mBottomSheetBehavior = (QueueStateBottomSheetBehavior<ViewGroup>) BottomSheetBehavior.from(
221                 mBottomSheetView);
222         mBottomSheetBehavior.setState(STATE_COLLAPSED);
223         mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetCallback() {
224             @Override
225             public void onStateChanged(@NonNull View bottomSheet, int newState) {
226                 if (mBottomSheetBehavior.isQueueProcessing()) {
227                     // Avoid button and bottom sheet mismatching from quick tapping buttons when
228                     // bottom sheet is changing state.
229                     disableActions();
230                     // If bottom sheet is going with expanded-collapsed-expanded, the new content
231                     // will be updated in collapsed state. The first state change from expanded to
232                     // collapsed should still show the previous content view.
233                     if (mSelectedAction != null && newState == STATE_COLLAPSED) {
234                         updateContentViewFor(mSelectedAction);
235                     }
236                     return;
237                 }
238 
239                 notifyAccessibilityCallback(newState);
240 
241                 // Enable all buttons when queue is not processing.
242                 enableActions();
243                 if (!isExpandable(mSelectedAction)) {
244                     return;
245                 }
246                 // Ensure the button state is the same as bottom sheet state to catch up the state
247                 // change from dragging or some unexpected bottom sheet state changes.
248                 if (newState == STATE_COLLAPSED) {
249                     updateSelectedState(mSelectedAction, /* selected= */ false);
250                 } else if (newState == STATE_EXPANDED) {
251                     updateSelectedState(mSelectedAction, /* selected= */ true);
252                 }
253             }
254             @Override
255             public void onSlide(@NonNull View bottomSheet, float slideOffset) { }
256         });
257 
258         setOnApplyWindowInsetsListener((v, windowInsets) -> {
259             v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(),
260                     windowInsets.getSystemWindowInsetBottom());
261             return windowInsets;
262         });
263 
264         // Skip "info selected" and "customize selected" Talkback while double tapping on info and
265         // customize action.
266         skipAccessibilityEvent(new BottomAction[]{BottomAction.INFORMATION, BottomAction.CUSTOMIZE},
267                 new int[]{AccessibilityEvent.TYPE_VIEW_CLICKED,
268                         AccessibilityEvent.TYPE_VIEW_SELECTED});
269     }
270 
271     @Override
onVisibilityAggregated(boolean isVisible)272     public void onVisibilityAggregated(boolean isVisible) {
273         super.onVisibilityAggregated(isVisible);
274         mVisibilityChangeListeners.forEach(listener -> listener.onVisibilityChange(isVisible));
275     }
276 
277     /**
278      * Binds the {@code bottomSheetContent} with the {@code action}, the {@code action} button
279      * would be able to expand/collapse the bottom sheet to show the content.
280      *
281      * @param bottomSheetContent the content object with view being added to the bottom sheet
282      * @param action the action to be bound to expand / collapse the bottom sheet
283      */
bindBottomSheetContentWithAction(BottomSheetContent<?> bottomSheetContent, BottomAction action)284     public void bindBottomSheetContentWithAction(BottomSheetContent<?> bottomSheetContent,
285             BottomAction action) {
286         mContentViewMap.put(action, bottomSheetContent);
287         mBottomSheetView.addView(bottomSheetContent.mContentView);
288         setActionClickListener(action, actionView -> {
289             if (mBottomSheetBehavior.getState() == STATE_COLLAPSED) {
290                 updateContentViewFor(action);
291             }
292             mBottomSheetView.setAccessibilityTraversalAfter(actionView.getId());
293         });
294     }
295 
296     /** Collapses the bottom sheet. */
collapseBottomSheetIfExpanded()297     public void collapseBottomSheetIfExpanded() {
298         hideBottomSheetAndDeselectButtonIfExpanded();
299     }
300 
301     /** Enables or disables action buttons that show the bottom sheet. */
enableActionButtonsWithBottomSheet(boolean enabled)302     public void enableActionButtonsWithBottomSheet(boolean enabled) {
303         if (enabled) {
304             enableActions(mContentViewMap.keySet().toArray(new BottomAction[0]));
305         } else {
306             disableActions(mContentViewMap.keySet().toArray(new BottomAction[0]));
307         }
308     }
309 
310     /**
311      * Sets a click listener to a specific action.
312      *
313      * @param bottomAction the specific action
314      * @param actionClickListener the click listener for the action
315      */
setActionClickListener( BottomAction bottomAction, OnClickListener actionClickListener)316     public void setActionClickListener(
317             BottomAction bottomAction, OnClickListener actionClickListener) {
318         View buttonView = mActionMap.get(bottomAction);
319         if (buttonView.hasOnClickListeners()) {
320             throw new IllegalStateException(
321                     "Had already set a click listener to button: " + bottomAction);
322         }
323         buttonView.setOnClickListener(view -> {
324             if (mSelectedAction != null && isActionSelected(mSelectedAction)) {
325                 updateSelectedState(mSelectedAction, /* selected= */ false);
326                 if (isExpandable(mSelectedAction)) {
327                     mBottomSheetBehavior.enqueue(STATE_COLLAPSED);
328                 }
329             } else {
330                 // Error handling, set to null if the action is not selected.
331                 mSelectedAction = null;
332             }
333 
334             if (bottomAction == mSelectedAction) {
335                 // Deselect the selected action.
336                 mSelectedAction = null;
337             } else {
338                 // Select a different action from the current selected action.
339                 // Also keep the same action for unselected case for a11y.
340                 mLastSelectedAction = mSelectedAction = bottomAction;
341                 updateSelectedState(mSelectedAction, /* selected= */ true);
342                 if (isExpandable(mSelectedAction)) {
343                     mBottomSheetBehavior.enqueue(STATE_EXPANDED);
344                 }
345             }
346             actionClickListener.onClick(view);
347             mBottomSheetBehavior.processQueueForStateChange();
348         });
349     }
350 
351     /**
352      * Sets a selected listener to a specific action. This is triggered each time the bottom
353      * action's selected state changes.
354      *
355      * @param bottomAction the specific action
356      * @param actionSelectedListener the selected listener for the action
357      */
setActionSelectedListener( BottomAction bottomAction, OnActionSelectedListener actionSelectedListener)358     public void setActionSelectedListener(
359             BottomAction bottomAction, OnActionSelectedListener actionSelectedListener) {
360         if (mActionSelectedListeners.containsKey(bottomAction)) {
361             throw new IllegalStateException(
362                     "Had already set a selected listener to button: " + bottomAction);
363         }
364         mActionSelectedListeners.put(bottomAction, actionSelectedListener);
365     }
366 
367     /** Set back button visibility. */
setBackButtonVisibility(int visibility)368     public void setBackButtonVisibility(int visibility) {
369         findViewById(R.id.action_back).setVisibility(visibility);
370     }
371 
372     /** Binds the cancel button to back key. */
bindBackButtonToSystemBackKey(Activity activity)373     public void bindBackButtonToSystemBackKey(Activity activity) {
374         findViewById(R.id.action_back).setOnClickListener(v -> activity.onBackPressed());
375     }
376 
377     /** Returns {@code true} if visible. */
isVisible()378     public boolean isVisible() {
379         return getVisibility() == VISIBLE;
380     }
381 
382     /** Shows {@link BottomActionBar}. */
show()383     public void show() {
384         setVisibility(VISIBLE);
385     }
386 
387     /** Hides {@link BottomActionBar}. */
hide()388     public void hide() {
389         setVisibility(GONE);
390     }
391 
392     /**
393      * Adds the visibility change listener.
394      *
395      * @param visibilityChangeListener the listener to be notified.
396      */
addVisibilityChangeListener(VisibilityChangeListener visibilityChangeListener)397     public void addVisibilityChangeListener(VisibilityChangeListener visibilityChangeListener) {
398         if (visibilityChangeListener == null) {
399             return;
400         }
401         mVisibilityChangeListeners.add(visibilityChangeListener);
402         visibilityChangeListener.onVisibilityChange(isVisible());
403     }
404 
405     /**
406      * Sets a AccessibilityCallback.
407      *
408      * @param accessibilityCallback the callback to be notified.
409      */
setAccessibilityCallback(@ullable AccessibilityCallback accessibilityCallback)410     public void setAccessibilityCallback(@Nullable AccessibilityCallback accessibilityCallback) {
411         mAccessibilityCallback = accessibilityCallback;
412     }
413 
414     /**
415      * Shows the specific actions.
416      *
417      * @param actions the specific actions
418      */
showActions(BottomAction... actions)419     public void showActions(BottomAction... actions) {
420         for (BottomAction action : actions) {
421             mActionMap.get(action).setVisibility(VISIBLE);
422         }
423     }
424 
425     /**
426      * Hides the specific actions.
427      *
428      * @param actions the specific actions
429      */
hideActions(BottomAction... actions)430     public void hideActions(BottomAction... actions) {
431         for (BottomAction action : actions) {
432             mActionMap.get(action).setVisibility(GONE);
433 
434             if (isExpandable(action) && mSelectedAction == action) {
435                 hideBottomSheetAndDeselectButtonIfExpanded();
436             }
437         }
438     }
439 
440     /**
441      * Focus the specific action.
442      *
443      * @param action the specific action
444      */
focusAccessibilityAction(BottomAction action)445     public void focusAccessibilityAction(BottomAction action) {
446         mActionMap.get(action).sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
447     }
448 
449     /**
450      * Shows the specific actions only. In other words, the other actions will be hidden.
451      *
452      * @param actions the specific actions which will be shown. Others will be hidden.
453      */
showActionsOnly(BottomAction... actions)454     public void showActionsOnly(BottomAction... actions) {
455         final Set<BottomAction> actionsSet = new HashSet<>(Arrays.asList(actions));
456 
457         mActionMap.keySet().forEach(action -> {
458             if (actionsSet.contains(action)) {
459                 showActions(action);
460             } else {
461                 hideActions(action);
462             }
463         });
464     }
465 
466     /**
467      * Checks if the specific actions are shown.
468      *
469      * @param actions the specific actions to be verified
470      * @return {@code true} if the actions are shown; {@code false} otherwise
471      */
areActionsShown(BottomAction... actions)472     public boolean areActionsShown(BottomAction... actions) {
473         final Set<BottomAction> actionsSet = new HashSet<>(Arrays.asList(actions));
474         return actionsSet.stream().allMatch(bottomAction -> {
475             View view = mActionMap.get(bottomAction);
476             return view != null && view.getVisibility() == VISIBLE;
477         });
478     }
479 
480     /**
481      * All actions will be hidden.
482      */
hideAllActions()483     public void hideAllActions() {
484         showActionsOnly(/* No actions to show */);
485     }
486 
487     /** Enables all the actions' {@link View}. */
enableActions()488     public void enableActions() {
489         enableActions(BottomAction.values());
490     }
491 
492     /** Disables all the actions' {@link View}. */
disableActions()493     public void disableActions() {
494         disableActions(BottomAction.values());
495     }
496 
497     /**
498      * Enables specified actions' {@link View}.
499      *
500      * @param actions the specified actions to enable their views
501      */
enableActions(BottomAction... actions)502     public void enableActions(BottomAction... actions) {
503         for (BottomAction action : actions) {
504             mActionMap.get(action).setEnabled(true);
505         }
506     }
507 
508     /**
509      * Disables specified actions' {@link View}.
510      *
511      * @param actions the specified actions to disable their views
512      */
disableActions(BottomAction... actions)513     public void disableActions(BottomAction... actions) {
514         for (BottomAction action : actions) {
515             mActionMap.get(action).setEnabled(false);
516         }
517     }
518 
519     /** Sets a default selected action button. */
setDefaultSelectedButton(BottomAction action)520     public void setDefaultSelectedButton(BottomAction action) {
521         if (mSelectedAction == null) {
522             mSelectedAction = action;
523             updateSelectedState(mSelectedAction, /* selected= */ true);
524         }
525     }
526 
527     /** Deselects an action button. */
deselectAction(BottomAction action)528     public void deselectAction(BottomAction action) {
529         if (isExpandable(action)) {
530             mBottomSheetBehavior.setState(STATE_COLLAPSED);
531         }
532         updateSelectedState(action, /* selected= */ false);
533         if (action == mSelectedAction) {
534             mSelectedAction = null;
535         }
536     }
537 
isActionSelected(BottomAction action)538     public boolean isActionSelected(BottomAction action) {
539         return mActionMap.get(action).isSelected();
540     }
541 
542     /** Returns {@code true} if the state of bottom sheet is collapsed. */
isBottomSheetCollapsed()543     public boolean isBottomSheetCollapsed() {
544         return mBottomSheetBehavior.getState() == STATE_COLLAPSED;
545     }
546 
547     /** Resets {@link BottomActionBar} to initial state. */
reset()548     public void reset() {
549         // Not visible by default, see res/layout/bottom_action_bar.xml
550         hide();
551         // All actions are hide and enabled by default, see res/layout/bottom_action_bar.xml
552         hideAllActions();
553         enableActions();
554         // Clears all the actions' click listeners
555         mActionMap.values().forEach(v -> v.setOnClickListener(null));
556         findViewById(R.id.action_back).setOnClickListener(null);
557         // Deselect all buttons.
558         mActionMap.keySet().forEach(a -> updateSelectedState(a, /* selected= */ false));
559         // Clear values.
560         mContentViewMap.clear();
561         mActionSelectedListeners.clear();
562         mBottomSheetView.removeAllViews();
563         mBottomSheetBehavior.reset();
564         mSelectedAction = null;
565     }
566 
567     /** Dynamic update color with {@code Context}. */
setColor(Context context)568     public void setColor(Context context) {
569         // Set bottom sheet background.
570         mBottomSheetView.setBackground(context.getDrawable(R.drawable.bottom_sheet_background));
571         if (mBottomSheetView.getChildCount() > 0) {
572             // Update the bottom sheet content view if any.
573             mBottomSheetView.removeAllViews();
574             mContentViewMap.values().forEach(bottomSheetContent -> {
575                 bottomSheetContent.recreateView(context);
576                 mBottomSheetView.addView(bottomSheetContent.mContentView);
577             });
578         }
579 
580         // Set the bar background and action buttons.
581         ViewGroup actionTabs = findViewById(R.id.action_tabs);
582         actionTabs.setBackgroundColor(
583                 ResourceUtils.getColorAttr(context, android.R.attr.colorBackground));
584         ColorStateList colorStateList = context.getColorStateList(
585                 R.color.bottom_action_button_color_tint);
586         for (int i = 0; i < actionTabs.getChildCount(); i++) {
587             View v = actionTabs.getChildAt(i);
588             if (v instanceof ImageView) {
589                 v.setBackground(context.getDrawable(R.drawable.bottom_action_button_background));
590                 ImageViewCompat.setImageTintList((ImageView) v, colorStateList);
591             } else if (v instanceof ProgressBar) {
592                 ((ProgressBar) v).setIndeterminateTintList(colorStateList);
593             }
594         }
595     }
596 
597     /** Sets action button accessibility traversal after. */
setActionAccessibilityTraversalAfter(BottomAction action, int afterId)598     public void setActionAccessibilityTraversalAfter(BottomAction action, int afterId) {
599         View bottomActionView = mActionMap.get(action);
600         bottomActionView.setAccessibilityTraversalAfter(afterId);
601     }
602 
603     /** Sets action button accessibility traversal before. */
setActionAccessibilityTraversalBefore(BottomAction action, int beforeId)604     public void setActionAccessibilityTraversalBefore(BottomAction action, int beforeId) {
605         View bottomActionView = mActionMap.get(action);
606         bottomActionView.setAccessibilityTraversalBefore(beforeId);
607     }
608 
updateSelectedState(BottomAction bottomAction, boolean selected)609     private void updateSelectedState(BottomAction bottomAction, boolean selected) {
610         View bottomActionView = mActionMap.get(bottomAction);
611         if (bottomActionView.isSelected() == selected) {
612             return;
613         }
614 
615         OnActionSelectedListener listener = mActionSelectedListeners.get(bottomAction);
616         if (listener != null) {
617             listener.onActionSelected(selected);
618         }
619         bottomActionView.setSelected(selected);
620     }
621 
hideBottomSheetAndDeselectButtonIfExpanded()622     private void hideBottomSheetAndDeselectButtonIfExpanded() {
623         if (isExpandable(mSelectedAction) && mBottomSheetBehavior.getState() == STATE_EXPANDED) {
624             mBottomSheetBehavior.setState(STATE_COLLAPSED);
625             updateSelectedState(mSelectedAction, /* selected= */ false);
626             mSelectedAction = null;
627         }
628     }
629 
updateContentViewFor(BottomAction action)630     private void updateContentViewFor(BottomAction action) {
631         mContentViewMap.forEach((a, content) -> content.setVisibility(a.equals(action)));
632     }
633 
isExpandable(BottomAction action)634     private boolean isExpandable(BottomAction action) {
635         return action != null && mContentViewMap.containsKey(action);
636     }
637 
notifyAccessibilityCallback(int state)638     private void notifyAccessibilityCallback(int state) {
639         if (mAccessibilityCallback == null) {
640             return;
641         }
642 
643         if (state == STATE_COLLAPSED) {
644             CharSequence text = getAccessibilityText(mLastSelectedAction, /* isShown= */ false);
645             if (!TextUtils.isEmpty(text)) {
646                 setAccessibilityPaneTitle(text);
647             }
648             mAccessibilityCallback.onBottomSheetCollapsed();
649         } else if (state == STATE_EXPANDED) {
650             CharSequence text = getAccessibilityText(mSelectedAction, /* isShown= */ true);
651             if (!TextUtils.isEmpty(text)) {
652                 setAccessibilityPaneTitle(text);
653             }
654             mAccessibilityCallback.onBottomSheetExpanded();
655         }
656     }
657 
getAccessibilityText(BottomAction action, boolean isShown)658     private CharSequence getAccessibilityText(BottomAction action, boolean isShown) {
659         if (action == null) {
660             return null;
661         }
662         int resId = action.getAccessibilityStringRes(isShown);
663         if (resId != 0) {
664             return mContext.getText(resId);
665         }
666         return null;
667     }
668 
669     /**
670      * Skip bottom action's Accessibility event.
671      *
672      * @param actions the {@link BottomAction} actions to be skipped.
673      * @param eventTypes the {@link AccessibilityEvent} event types to be skipped.
674      */
skipAccessibilityEvent(BottomAction[] actions, int[] eventTypes)675     private void skipAccessibilityEvent(BottomAction[] actions, int[] eventTypes) {
676         for (BottomAction action : actions) {
677             View view = mActionMap.get(action);
678             view.setAccessibilityDelegate(new AccessibilityDelegate() {
679                 @Override
680                 public void sendAccessibilityEvent(View host, int eventType) {
681                     if (!ArrayUtils.contains(eventTypes, eventType)) {
682                         super.sendAccessibilityEvent(host, eventType);
683                     }
684                 }
685             });
686         }
687     }
688 
689     /** A {@link BottomSheetBehavior} that can process a queue of bottom sheet states.*/
690     public static class QueueStateBottomSheetBehavior<V extends View>
691             extends BottomSheetBehavior<V> {
692 
693         private final Deque<Integer> mStateQueue = new ArrayDeque<>();
694         private boolean mIsQueueProcessing;
695 
QueueStateBottomSheetBehavior(Context context, @Nullable AttributeSet attrs)696         public QueueStateBottomSheetBehavior(Context context, @Nullable AttributeSet attrs) {
697             super(context, attrs);
698             // Binds the default callback for processing queue.
699             setBottomSheetCallback(null);
700         }
701 
702         /** Enqueues the bottom sheet states. */
enqueue(int state)703         public void enqueue(int state) {
704             if (!mStateQueue.isEmpty() && mStateQueue.getLast() == state) {
705                 return;
706             }
707             mStateQueue.add(state);
708         }
709 
710         /** Processes the queue of bottom sheet state that was set via {@link #enqueue}. */
processQueueForStateChange()711         public void processQueueForStateChange() {
712             if (mStateQueue.isEmpty()) {
713                 return;
714             }
715             setState(mStateQueue.getFirst());
716             mIsQueueProcessing = true;
717         }
718 
719         /**
720          * Returns {@code true} if the queue is processing. For example, if the bottom sheet is
721          * going with expanded-collapsed-expanded, it would return {@code true} until last expanded
722          * state is finished.
723          */
isQueueProcessing()724         public boolean isQueueProcessing() {
725             return mIsQueueProcessing;
726         }
727 
728         /** Resets the queue state. */
reset()729         public void reset() {
730             mStateQueue.clear();
731             mIsQueueProcessing = false;
732         }
733 
734         @Override
setBottomSheetCallback(BottomSheetCallback callback)735         public void setBottomSheetCallback(BottomSheetCallback callback) {
736             super.setBottomSheetCallback(new BottomSheetCallback() {
737                 @Override
738                 public void onStateChanged(@NonNull View bottomSheet, int newState) {
739                     if (!mStateQueue.isEmpty()) {
740                         if (newState == mStateQueue.getFirst()) {
741                             mStateQueue.removeFirst();
742                             if (mStateQueue.isEmpty()) {
743                                 mIsQueueProcessing = false;
744                             } else {
745                                 setState(mStateQueue.getFirst());
746                             }
747                         } else {
748                             setState(mStateQueue.getFirst());
749                         }
750                     }
751 
752                     if (callback != null) {
753                         callback.onStateChanged(bottomSheet, newState);
754                     }
755                 }
756 
757                 @Override
758                 public void onSlide(@NonNull View bottomSheet, float slideOffset) {
759                     if (callback != null) {
760                         callback.onSlide(bottomSheet, slideOffset);
761                     }
762                 }
763             });
764         }
765     }
766 }
767