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