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