1 /*
2  * Copyright (C) 2021 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.launcher3.taskbar;
17 
18 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
19 
20 import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR;
21 import static com.android.launcher3.Flags.enableCursorHoverStates;
22 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
23 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
24 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR;
25 import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
26 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
27 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
28 
29 import android.content.Context;
30 import android.content.res.Resources;
31 import android.graphics.Canvas;
32 import android.graphics.Rect;
33 import android.os.Bundle;
34 import android.util.AttributeSet;
35 import android.view.DisplayCutout;
36 import android.view.InputDevice;
37 import android.view.LayoutInflater;
38 import android.view.MotionEvent;
39 import android.view.View;
40 import android.view.ViewConfiguration;
41 import android.view.accessibility.AccessibilityNodeInfo;
42 import android.widget.FrameLayout;
43 
44 import androidx.annotation.DimenRes;
45 import androidx.annotation.DrawableRes;
46 import androidx.annotation.LayoutRes;
47 import androidx.annotation.NonNull;
48 import androidx.annotation.Nullable;
49 
50 import com.android.launcher3.BubbleTextView;
51 import com.android.launcher3.DeviceProfile;
52 import com.android.launcher3.Insettable;
53 import com.android.launcher3.R;
54 import com.android.launcher3.Utilities;
55 import com.android.launcher3.apppairs.AppPairIcon;
56 import com.android.launcher3.folder.FolderIcon;
57 import com.android.launcher3.folder.PreviewBackground;
58 import com.android.launcher3.model.data.AppPairInfo;
59 import com.android.launcher3.model.data.CollectionInfo;
60 import com.android.launcher3.model.data.FolderInfo;
61 import com.android.launcher3.model.data.ItemInfo;
62 import com.android.launcher3.model.data.WorkspaceItemInfo;
63 import com.android.launcher3.util.DisplayController;
64 import com.android.launcher3.util.LauncherBindableItemsContainer;
65 import com.android.launcher3.util.Themes;
66 import com.android.launcher3.views.ActivityContext;
67 import com.android.launcher3.views.IconButtonView;
68 import com.android.quickstep.DeviceConfigWrapper;
69 import com.android.quickstep.util.AssistStateManager;
70 
71 import java.util.function.Predicate;
72 
73 /**
74  * Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
75  */
76 public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconParent, Insettable,
77         DeviceProfile.OnDeviceProfileChangeListener {
78     private static final Rect sTmpRect = new Rect();
79 
80     private final int[] mTempOutLocation = new int[2];
81     private final Rect mIconLayoutBounds;
82     private final int mIconTouchSize;
83     private final int mItemMarginLeftRight;
84     private final int mItemPadding;
85     private final int mFolderLeaveBehindColor;
86     private final boolean mIsRtl;
87 
88     private final TaskbarActivityContext mActivityContext;
89 
90     // Initialized in init.
91     private TaskbarViewCallbacks mControllerCallbacks;
92     private View.OnClickListener mIconClickListener;
93     private View.OnLongClickListener mIconLongClickListener;
94 
95     // Only non-null when the corresponding Folder is open.
96     private @Nullable FolderIcon mLeaveBehindFolderIcon;
97 
98     // Only non-null when device supports having an All Apps button.
99     private @Nullable IconButtonView mAllAppsButton;
100     private Runnable mAllAppsTouchRunnable;
101     private long mAllAppsButtonTouchDelayMs;
102     private boolean mAllAppsTouchTriggered;
103 
104     // Only non-null when device supports having an All Apps button.
105     private @Nullable IconButtonView mTaskbarDivider;
106 
107     private final View mQsb;
108 
109     private final float mTransientTaskbarMinWidth;
110 
111     private boolean mShouldTryStartAlign;
112 
TaskbarView(@onNull Context context)113     public TaskbarView(@NonNull Context context) {
114         this(context, null);
115     }
116 
TaskbarView(@onNull Context context, @Nullable AttributeSet attrs)117     public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs) {
118         this(context, attrs, 0);
119     }
120 
TaskbarView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)121     public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs,
122             int defStyleAttr) {
123         this(context, attrs, defStyleAttr, 0);
124     }
125 
TaskbarView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)126     public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
127             int defStyleRes) {
128         super(context, attrs, defStyleAttr, defStyleRes);
129         mActivityContext = ActivityContext.lookupContext(context);
130         mIconLayoutBounds = mActivityContext.getTransientTaskbarBounds();
131         Resources resources = getResources();
132         boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivityContext)
133                 && !mActivityContext.isPhoneMode();
134         mIsRtl = Utilities.isRtl(resources);
135         mTransientTaskbarMinWidth = resources.getDimension(R.dimen.transient_taskbar_min_width);
136 
137         onDeviceProfileChanged(mActivityContext.getDeviceProfile());
138 
139         int actualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
140         int actualIconSize = mActivityContext.getDeviceProfile().taskbarIconSize;
141         if (enableTaskbarPinning() && !mActivityContext.isThreeButtonNav()) {
142             DeviceProfile deviceProfile = mActivityContext.getTransientTaskbarDeviceProfile();
143             actualIconSize = deviceProfile.taskbarIconSize;
144         }
145         int visualIconSize = (int) (actualIconSize * ICON_VISIBLE_AREA_FACTOR);
146 
147         mIconTouchSize = Math.max(actualIconSize,
148                 resources.getDimensionPixelSize(R.dimen.taskbar_icon_min_touch_size));
149 
150         // We layout the icons to be of mIconTouchSize in width and height
151         mItemMarginLeftRight = actualMargin - (mIconTouchSize - visualIconSize) / 2;
152 
153         // We always layout taskbar as a transient taskbar when we have taskbar pinning feature on,
154         // then we scale and translate the icons to match persistent taskbar designs, so we use
155         // taskbar icon size from current device profile to calculate correct item padding.
156         mItemPadding = (mIconTouchSize - mActivityContext.getDeviceProfile().taskbarIconSize) / 2;
157         mFolderLeaveBehindColor = Themes.getAttrColor(mActivityContext,
158                 android.R.attr.textColorTertiary);
159 
160         // Needed to draw folder leave-behind when opening one.
161         setWillNotDraw(false);
162 
163         mAllAppsButton = (IconButtonView) LayoutInflater.from(context)
164                 .inflate(R.layout.taskbar_all_apps_button, this, false);
165         mAllAppsButton.setIconDrawable(resources.getDrawable(
166                 getAllAppsButton(isTransientTaskbar)));
167         mAllAppsButton.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
168         mAllAppsButton.setForegroundTint(
169                 mActivityContext.getColor(R.color.all_apps_button_color));
170 
171         if (enableTaskbarPinning()) {
172             mTaskbarDivider = (IconButtonView) LayoutInflater.from(context).inflate(
173                     R.layout.taskbar_divider,
174                     this, false);
175             mTaskbarDivider.setIconDrawable(
176                     resources.getDrawable(R.drawable.taskbar_divider_button));
177             mTaskbarDivider.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
178         }
179 
180         // TODO: Disable touch events on QSB otherwise it can crash.
181         mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
182 
183         // Default long press (touch) delay = 400ms
184         mAllAppsButtonTouchDelayMs = ViewConfiguration.getLongPressTimeout();
185     }
186 
187     @DrawableRes
getAllAppsButton(boolean isTransientTaskbar)188     private int getAllAppsButton(boolean isTransientTaskbar) {
189         boolean shouldSelectTransientIcon =
190                 (isTransientTaskbar || enableTaskbarPinning())
191                 && !mActivityContext.isThreeButtonNav();
192         if (ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
193             return shouldSelectTransientIcon
194                     ? R.drawable.ic_transient_taskbar_all_apps_search_button
195                     : R.drawable.ic_taskbar_all_apps_search_button;
196         } else {
197             return shouldSelectTransientIcon
198                     ? R.drawable.ic_transient_taskbar_all_apps_button
199                     : R.drawable.ic_taskbar_all_apps_button;
200         }
201     }
202 
203     @DimenRes
getAllAppsButtonTranslationXOffset(boolean isTransientTaskbar)204     public int getAllAppsButtonTranslationXOffset(boolean isTransientTaskbar) {
205         if (isTransientTaskbar) {
206             return R.dimen.transient_taskbar_all_apps_button_translation_x_offset;
207         } else {
208             return ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()
209                     ? R.dimen.taskbar_all_apps_search_button_translation_x_offset
210                     : R.dimen.taskbar_all_apps_button_translation_x_offset;
211         }
212     }
213 
214     @Override
setVisibility(int visibility)215     public void setVisibility(int visibility) {
216         boolean changed = getVisibility() != visibility;
217         super.setVisibility(visibility);
218         if (changed && mControllerCallbacks != null) {
219             mControllerCallbacks.notifyVisibilityChanged();
220         }
221     }
222 
223     @Override
onAttachedToWindow()224     protected void onAttachedToWindow() {
225         super.onAttachedToWindow();
226         mActivityContext.addOnDeviceProfileChangeListener(this);
227     }
228 
229     @Override
onDetachedFromWindow()230     protected void onDetachedFromWindow() {
231         super.onDetachedFromWindow();
232         mActivityContext.removeOnDeviceProfileChangeListener(this);
233     }
234 
235     @Override
onDeviceProfileChanged(DeviceProfile dp)236     public void onDeviceProfileChanged(DeviceProfile dp) {
237         mShouldTryStartAlign = mActivityContext.isThreeButtonNav() && dp.startAlignTaskbar;
238     }
239 
240     @Override
performAccessibilityActionInternal(int action, Bundle arguments)241     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
242         if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
243             announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title));
244         } else if (action == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
245             announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title));
246         }
247         return super.performAccessibilityActionInternal(action, arguments);
248 
249     }
250 
announceAccessibilityChanges()251     protected void announceAccessibilityChanges() {
252         this.performAccessibilityAction(
253                 isVisibleToUser() ? AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS
254                         : AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
255 
256         ActivityContext.lookupContext(getContext()).getDragLayer()
257                 .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
258     }
259 
260     /**
261      * Returns the icon touch size.
262      */
getIconTouchSize()263     public int getIconTouchSize() {
264         return mIconTouchSize;
265     }
266 
init(TaskbarViewCallbacks callbacks)267     protected void init(TaskbarViewCallbacks callbacks) {
268         // set taskbar pane title so that accessibility service know it window and focuses.
269         setAccessibilityPaneTitle(getContext().getString(R.string.taskbar_a11y_title));
270         mControllerCallbacks = callbacks;
271         mIconClickListener = mControllerCallbacks.getIconOnClickListener();
272         mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener();
273 
274         if (mAllAppsButton != null) {
275             mAllAppsButton.setOnClickListener(this::onAllAppsButtonClick);
276             mAllAppsButton.setOnLongClickListener(this::onAllAppsButtonLongClick);
277             mAllAppsButton.setOnTouchListener(this::onAllAppsButtonTouch);
278             mAllAppsButton.setHapticFeedbackEnabled(
279                     mControllerCallbacks.isAllAppsButtonHapticFeedbackEnabled());
280             mAllAppsTouchRunnable = () -> {
281                 mControllerCallbacks.triggerAllAppsButtonLongClick();
282                 mAllAppsTouchTriggered = true;
283             };
284             AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(mContext);
285             if (DeviceConfigWrapper.get().getCustomLpaaThresholds()
286                     && assistStateManager.getLPNHDurationMillis().isPresent()) {
287                 mAllAppsButtonTouchDelayMs = assistStateManager.getLPNHDurationMillis().get();
288             }
289         }
290         if (mTaskbarDivider != null && !mActivityContext.isThreeButtonNav()) {
291             mTaskbarDivider.setOnLongClickListener(
292                     mControllerCallbacks.getTaskbarDividerLongClickListener());
293             mTaskbarDivider.setOnTouchListener(
294                     mControllerCallbacks.getTaskbarDividerRightClickListener());
295         }
296     }
297 
removeAndRecycle(View view)298     private void removeAndRecycle(View view) {
299         removeView(view);
300         view.setOnClickListener(null);
301         view.setOnLongClickListener(null);
302         if (!(view.getTag() instanceof CollectionInfo)) {
303             mActivityContext.getViewCache().recycleView(view.getSourceLayoutResId(), view);
304         }
305         view.setTag(null);
306     }
307 
308     /**
309      * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
310      */
updateHotseatItems(ItemInfo[] hotseatItemInfos)311     protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
312         int nextViewIndex = 0;
313         int numViewsAnimated = 0;
314 
315         if (mAllAppsButton != null) {
316             removeView(mAllAppsButton);
317 
318             if (mTaskbarDivider != null) {
319                 removeView(mTaskbarDivider);
320             }
321         }
322         removeView(mQsb);
323 
324         for (int i = 0; i < hotseatItemInfos.length; i++) {
325             ItemInfo hotseatItemInfo = hotseatItemInfos[i];
326             if (hotseatItemInfo == null) {
327                 continue;
328             }
329 
330             // Replace any Hotseat views with the appropriate type if it's not already that type.
331             final int expectedLayoutResId;
332             boolean isCollection = false;
333             if (hotseatItemInfo.isPredictedItem()) {
334                 expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
335             } else if (hotseatItemInfo instanceof CollectionInfo ci) {
336                 expectedLayoutResId = ci.itemType == ITEM_TYPE_APP_PAIR
337                         ? R.layout.app_pair_icon
338                         : R.layout.folder_icon;
339                 isCollection = true;
340             } else {
341                 expectedLayoutResId = R.layout.taskbar_app_icon;
342             }
343 
344             View hotseatView = null;
345             while (nextViewIndex < getChildCount()) {
346                 hotseatView = getChildAt(nextViewIndex);
347 
348                 // see if the view can be reused
349                 if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId)
350                         || (isCollection && (hotseatView.getTag() != hotseatItemInfo))) {
351                     // Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation,
352                     // so if the info changes we need to reinflate. This should only happen if a new
353                     // folder is dragged to the position that another folder previously existed.
354                     removeAndRecycle(hotseatView);
355                     hotseatView = null;
356                 } else {
357                     // View found
358                     break;
359                 }
360             }
361 
362             if (hotseatView == null) {
363                 if (isCollection) {
364                     CollectionInfo collectionInfo = (CollectionInfo) hotseatItemInfo;
365                     switch (hotseatItemInfo.itemType) {
366                         case ITEM_TYPE_FOLDER:
367                             hotseatView = FolderIcon.inflateFolderAndIcon(
368                                     expectedLayoutResId, mActivityContext, this,
369                                     (FolderInfo) collectionInfo);
370                             ((FolderIcon) hotseatView).setTextVisible(false);
371                             break;
372                         case ITEM_TYPE_APP_PAIR:
373                             hotseatView = AppPairIcon.inflateIcon(
374                                     expectedLayoutResId, mActivityContext, this,
375                                     (AppPairInfo) collectionInfo, DISPLAY_TASKBAR);
376                             ((AppPairIcon) hotseatView).setTextVisible(false);
377                             break;
378                         default:
379                             throw new IllegalStateException(
380                                     "Unexpected item type: " + hotseatItemInfo.itemType);
381                     }
382                 } else {
383                     hotseatView = inflate(expectedLayoutResId);
384                 }
385                 LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
386                 hotseatView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
387                 addView(hotseatView, nextViewIndex, lp);
388             }
389 
390             // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
391             if (hotseatView instanceof BubbleTextView
392                     && hotseatItemInfo instanceof WorkspaceItemInfo) {
393                 BubbleTextView btv = (BubbleTextView) hotseatView;
394                 WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo;
395 
396                 boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo);
397                 btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated);
398                 if (animate) {
399                     numViewsAnimated++;
400                 }
401             }
402             setClickAndLongClickListenersForIcon(hotseatView);
403             if (enableCursorHoverStates()) {
404                 setHoverListenerForIcon(hotseatView);
405             }
406             nextViewIndex++;
407         }
408         // Remove remaining views
409         while (nextViewIndex < getChildCount()) {
410             removeAndRecycle(getChildAt(nextViewIndex));
411         }
412 
413         if (mAllAppsButton != null) {
414             addView(mAllAppsButton, mIsRtl ? getChildCount() : 0);
415 
416             // if only all apps button present, don't include divider view.
417             if (mTaskbarDivider != null && getChildCount() > 1) {
418                 addView(mTaskbarDivider, mIsRtl ? (getChildCount() - 1) : 1);
419             }
420         }
421         if (mActivityContext.getDeviceProfile().isQsbInline) {
422             addView(mQsb, mIsRtl ? getChildCount() : 0);
423             // Always set QSB to invisible after re-adding.
424             mQsb.setVisibility(View.INVISIBLE);
425         }
426     }
427 
428     /**
429      * Sets OnClickListener and OnLongClickListener for the given view.
430      */
setClickAndLongClickListenersForIcon(View icon)431     public void setClickAndLongClickListenersForIcon(View icon) {
432         icon.setOnClickListener(mIconClickListener);
433         icon.setOnLongClickListener(mIconLongClickListener);
434         // Add right-click support to btv icons.
435         icon.setOnTouchListener((v, event) -> {
436             if (event.isFromSource(InputDevice.SOURCE_MOUSE)
437                     && (event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0
438                     && v instanceof BubbleTextView) {
439                 mActivityContext.showPopupMenuForIcon((BubbleTextView) v);
440                 return true;
441             }
442             return false;
443         });
444     }
445 
446     /**
447      * Sets OnHoverListener for the given view.
448      */
setHoverListenerForIcon(View icon)449     private void setHoverListenerForIcon(View icon) {
450         icon.setOnHoverListener(mControllerCallbacks.getIconOnHoverListener(icon));
451     }
452 
453     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)454     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
455         int count = getChildCount();
456         DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
457         int spaceNeeded = getIconLayoutWidth();
458         int navSpaceNeeded = deviceProfile.hotseatBarEndOffset;
459         boolean layoutRtl = isLayoutRtl();
460         int centerAlignIconEnd = right - (right - left - spaceNeeded) / 2;
461         int iconEnd;
462 
463         if (mShouldTryStartAlign) {
464             // Taskbar is aligned to the start
465             int startSpacingPx = deviceProfile.inlineNavButtonsEndSpacingPx;
466 
467             if (layoutRtl) {
468                 iconEnd = right - startSpacingPx;
469             } else {
470                 iconEnd = startSpacingPx + spaceNeeded;
471             }
472         } else {
473             iconEnd = centerAlignIconEnd;
474         }
475 
476         boolean needMoreSpaceForNav = layoutRtl
477                 ? navSpaceNeeded > (iconEnd - spaceNeeded)
478                 : iconEnd > (right - navSpaceNeeded);
479         if (needMoreSpaceForNav) {
480             // Add offset to account for nav bar when taskbar is centered
481             int offset = layoutRtl
482                     ? navSpaceNeeded - (centerAlignIconEnd - spaceNeeded)
483                     : (right - navSpaceNeeded) - centerAlignIconEnd;
484 
485             iconEnd = centerAlignIconEnd + offset;
486         }
487 
488         // Currently, we support only one device with display cutout and we only are concern about
489         // it when the bottom rect is present and non empty
490         DisplayCutout displayCutout = getDisplay().getCutout();
491         if (displayCutout != null && !displayCutout.getBoundingRectBottom().isEmpty()) {
492             Rect cutoutBottomRect = displayCutout.getBoundingRectBottom();
493             // when cutout present at the bottom of screen align taskbar icons to cutout offset
494             // if taskbar icon overlaps with cutout
495             int taskbarIconLeftBound = iconEnd - spaceNeeded;
496             int taskbarIconRightBound = iconEnd;
497 
498             boolean doesTaskbarIconsOverlapWithCutout =
499                     taskbarIconLeftBound <= cutoutBottomRect.centerX()
500                             && cutoutBottomRect.centerX() <= taskbarIconRightBound;
501 
502             if (doesTaskbarIconsOverlapWithCutout) {
503                 if (!layoutRtl) {
504                     iconEnd = spaceNeeded + cutoutBottomRect.width();
505                 } else {
506                     iconEnd = right - cutoutBottomRect.width();
507                 }
508             }
509         }
510 
511         sTmpRect.set(mIconLayoutBounds);
512 
513         // Layout the children
514         mIconLayoutBounds.right = iconEnd;
515         mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2;
516         mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize;
517         for (int i = count; i > 0; i--) {
518             View child = getChildAt(i - 1);
519             if (child == mQsb) {
520                 int qsbStart;
521                 int qsbEnd;
522                 if (layoutRtl) {
523                     qsbStart = iconEnd + mItemMarginLeftRight;
524                     qsbEnd = qsbStart + deviceProfile.hotseatQsbWidth;
525                 } else {
526                     qsbEnd = iconEnd - mItemMarginLeftRight;
527                     qsbStart = qsbEnd - deviceProfile.hotseatQsbWidth;
528                 }
529                 int qsbTop = (bottom - top - deviceProfile.hotseatQsbHeight) / 2;
530                 int qsbBottom = qsbTop + deviceProfile.hotseatQsbHeight;
531                 child.layout(qsbStart, qsbTop, qsbEnd, qsbBottom);
532             } else if (child == mTaskbarDivider) {
533                 iconEnd += mItemMarginLeftRight;
534                 int iconStart = iconEnd - mIconTouchSize;
535                 child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom);
536                 iconEnd = iconStart + mItemMarginLeftRight;
537             } else {
538                 iconEnd -= mItemMarginLeftRight;
539                 int iconStart = iconEnd - mIconTouchSize;
540                 child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom);
541                 iconEnd = iconStart - mItemMarginLeftRight;
542             }
543         }
544 
545         mIconLayoutBounds.left = iconEnd;
546 
547         if (mIconLayoutBounds.right - mIconLayoutBounds.left < mTransientTaskbarMinWidth) {
548             int center = mIconLayoutBounds.centerX();
549             int distanceFromCenter = (int) mTransientTaskbarMinWidth / 2;
550             mIconLayoutBounds.right = center + distanceFromCenter;
551             mIconLayoutBounds.left = center - distanceFromCenter;
552         }
553 
554         if (!sTmpRect.equals(mIconLayoutBounds)) {
555             mControllerCallbacks.notifyIconLayoutBoundsChanged();
556         }
557     }
558 
559     /**
560      * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
561      * touch bounds.
562      */
isEventOverAnyItem(MotionEvent ev)563     public boolean isEventOverAnyItem(MotionEvent ev) {
564         getLocationOnScreen(mTempOutLocation);
565         int xInOurCoordinates = (int) ev.getX() - mTempOutLocation[0];
566         int yInOurCoorindates = (int) ev.getY() - mTempOutLocation[1];
567         return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
568     }
569 
getIconLayoutBounds()570     public Rect getIconLayoutBounds() {
571         return mIconLayoutBounds;
572     }
573 
574     /**
575      * Returns the space used by the icons
576      */
getIconLayoutWidth()577     public int getIconLayoutWidth() {
578         int countExcludingQsb = getChildCount();
579         DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
580         if (deviceProfile.isQsbInline) {
581             countExcludingQsb--;
582         }
583         int iconLayoutBoundsWidth =
584                 countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize);
585 
586         if (enableTaskbarPinning() && countExcludingQsb > 1) {
587             // We are removing 4 * mItemMarginLeftRight as there should be no space between
588             // All Apps icon, divider icon, and first app icon in taskbar
589             iconLayoutBoundsWidth -= mItemMarginLeftRight * 4;
590         }
591         return iconLayoutBoundsWidth;
592     }
593 
594     /**
595      * Returns the app icons currently shown in the taskbar.
596      */
getIconViews()597     public View[] getIconViews() {
598         final int count = getChildCount();
599         View[] icons = new View[count];
600         for (int i = 0; i < count; i++) {
601             icons[i] = getChildAt(i);
602         }
603         return icons;
604     }
605 
606     /**
607      * Returns the all apps button in the taskbar.
608      */
609     @Nullable
getAllAppsButtonView()610     public View getAllAppsButtonView() {
611         return mAllAppsButton;
612     }
613 
614     /**
615      * Returns the taskbar divider in the taskbar.
616      */
617     @Nullable
getTaskbarDividerView()618     public View getTaskbarDividerView() {
619         return mTaskbarDivider;
620     }
621 
622     /**
623      * Returns the QSB in the taskbar.
624      */
getQsb()625     public View getQsb() {
626         return mQsb;
627     }
628 
629     // FolderIconParent implemented methods.
630 
631     @Override
drawFolderLeaveBehindForIcon(FolderIcon child)632     public void drawFolderLeaveBehindForIcon(FolderIcon child) {
633         mLeaveBehindFolderIcon = child;
634         invalidate();
635     }
636 
637     @Override
clearFolderLeaveBehind(FolderIcon child)638     public void clearFolderLeaveBehind(FolderIcon child) {
639         mLeaveBehindFolderIcon = null;
640         invalidate();
641     }
642 
643     // End FolderIconParent implemented methods.
644 
645     @Override
onDraw(Canvas canvas)646     protected void onDraw(Canvas canvas) {
647         super.onDraw(canvas);
648         if (mLeaveBehindFolderIcon != null) {
649             canvas.save();
650             canvas.translate(
651                     mLeaveBehindFolderIcon.getLeft() + mLeaveBehindFolderIcon.getTranslationX(),
652                     mLeaveBehindFolderIcon.getTop());
653             PreviewBackground previewBackground = mLeaveBehindFolderIcon.getFolderBackground();
654             previewBackground.drawLeaveBehind(canvas, mFolderLeaveBehindColor);
655             canvas.restore();
656         }
657     }
658 
inflate(@ayoutRes int layoutResId)659     private View inflate(@LayoutRes int layoutResId) {
660         return mActivityContext.getViewCache().getView(layoutResId, mActivityContext, this);
661     }
662 
663     @Override
setInsets(Rect insets)664     public void setInsets(Rect insets) {
665         // Ignore, we just implement Insettable to draw behind system insets.
666     }
667 
areIconsVisible()668     public boolean areIconsVisible() {
669         // Consider the overall visibility
670         return getVisibility() == VISIBLE;
671     }
672 
673     /**
674      * Maps {@code op} over all the child views.
675      */
mapOverItems(LauncherBindableItemsContainer.ItemOperator op)676     public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) {
677         // map over all the shortcuts on the taskbar
678         for (int i = 0; i < getChildCount(); i++) {
679             View item = getChildAt(i);
680             if (op.evaluate((ItemInfo) item.getTag(), item)) {
681                 return;
682             }
683         }
684     }
685 
686     /**
687      * Finds the first icon to match one of the given matchers, from highest to lowest priority.
688      *
689      * @return The first match, or All Apps button if no match was found.
690      */
getFirstMatch(Predicate<ItemInfo>.... matchers)691     public View getFirstMatch(Predicate<ItemInfo>... matchers) {
692         for (Predicate<ItemInfo> matcher : matchers) {
693             for (int i = 0; i < getChildCount(); i++) {
694                 View item = getChildAt(i);
695                 if (!(item.getTag() instanceof ItemInfo)) {
696                     // Should only happen for All Apps button.
697                     continue;
698                 }
699                 ItemInfo info = (ItemInfo) item.getTag();
700                 if (matcher.test(info)) {
701                     return item;
702                 }
703             }
704         }
705         return mAllAppsButton;
706     }
707 
onAllAppsButtonTouch(View view, MotionEvent ev)708     private boolean onAllAppsButtonTouch(View view, MotionEvent ev) {
709         switch (ev.getAction()) {
710             case MotionEvent.ACTION_DOWN:
711                 mAllAppsTouchTriggered = false;
712                 MAIN_EXECUTOR.getHandler().postDelayed(
713                         mAllAppsTouchRunnable, mAllAppsButtonTouchDelayMs);
714                 break;
715             case MotionEvent.ACTION_UP:
716             case MotionEvent.ACTION_CANCEL:
717                 cancelAllAppsButtonTouch();
718         }
719         return false;
720     }
721 
cancelAllAppsButtonTouch()722     private void cancelAllAppsButtonTouch() {
723         MAIN_EXECUTOR.getHandler().removeCallbacks(mAllAppsTouchRunnable);
724         // ACTION_UP is first triggered, then click listener / long-click listener is triggered on
725         // the next frame, so we need to post twice and delay the reset.
726         if (mAllAppsButton != null) {
727             mAllAppsButton.post(() -> {
728                 mAllAppsButton.post(() -> {
729                     mAllAppsTouchTriggered = false;
730                 });
731             });
732         }
733     }
734 
onAllAppsButtonClick(View view)735     private void onAllAppsButtonClick(View view) {
736         if (!mAllAppsTouchTriggered) {
737             mControllerCallbacks.triggerAllAppsButtonClick(view);
738         }
739     }
740 
741     // Handle long click from Switch Access and Voice Access
onAllAppsButtonLongClick(View view)742     private boolean onAllAppsButtonLongClick(View view) {
743         if (!MAIN_EXECUTOR.getHandler().hasCallbacks(mAllAppsTouchRunnable)
744                 && !mAllAppsTouchTriggered) {
745             mControllerCallbacks.triggerAllAppsButtonLongClick();
746         }
747         return true;
748     }
749 }
750