1 /* 2 * Copyright (C) 2024 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.wm.shell.windowdecor.viewholder 17 18 import android.annotation.ColorInt 19 import android.annotation.DrawableRes 20 import android.app.ActivityManager.RunningTaskInfo 21 import android.content.res.ColorStateList 22 import android.content.res.Configuration 23 import android.graphics.Bitmap 24 import android.graphics.Color 25 import android.graphics.Rect 26 import android.os.Bundle 27 import android.view.View 28 import android.view.View.OnLongClickListener 29 import android.view.ViewTreeObserver.OnGlobalLayoutListener 30 import android.view.accessibility.AccessibilityEvent 31 import android.view.accessibility.AccessibilityNodeInfo 32 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction 33 import android.widget.ImageButton 34 import android.widget.ImageView 35 import android.widget.TextView 36 import android.window.DesktopModeFlags 37 import androidx.compose.material3.dynamicDarkColorScheme 38 import androidx.compose.material3.dynamicLightColorScheme 39 import androidx.compose.ui.graphics.toArgb 40 import androidx.core.content.withStyledAttributes 41 import androidx.core.view.ViewCompat 42 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat 43 import androidx.core.view.isGone 44 import androidx.core.view.isVisible 45 import com.android.internal.R.color.materialColorOnSecondaryContainer 46 import com.android.internal.R.color.materialColorOnSurface 47 import com.android.internal.R.color.materialColorSecondaryContainer 48 import com.android.internal.R.color.materialColorSurfaceContainerHigh 49 import com.android.internal.R.color.materialColorSurfaceContainerLow 50 import com.android.internal.R.color.materialColorSurfaceDim 51 import com.android.wm.shell.R 52 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger 53 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_ACTION_MAXIMIZE_RESTORE 54 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_ACTION_RESIZE_LEFT 55 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_ACTION_RESIZE_RIGHT 56 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_WINDOW_CLOSE_BUTTON 57 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_WINDOW_MAXIMIZE_RESTORE_BUTTON 58 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_WINDOW_MINIMIZE_BUTTON 59 import com.android.wm.shell.windowdecor.MaximizeButtonView 60 import com.android.wm.shell.windowdecor.common.DecorThemeUtil 61 import com.android.wm.shell.windowdecor.common.DrawableInsets 62 import com.android.wm.shell.windowdecor.common.OPACITY_100 63 import com.android.wm.shell.windowdecor.common.OPACITY_55 64 import com.android.wm.shell.windowdecor.common.OPACITY_65 65 import com.android.wm.shell.windowdecor.common.Theme 66 import com.android.wm.shell.windowdecor.common.createBackgroundDrawable 67 import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance 68 import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance 69 70 /** 71 * A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts 72 * finer controls such as a close window button and an "app info" section to pull up additional 73 * controls. 74 */ 75 class AppHeaderViewHolder( 76 rootView: View, 77 onCaptionTouchListener: View.OnTouchListener, 78 onCaptionButtonClickListener: View.OnClickListener, 79 private val onLongClickListener: OnLongClickListener, 80 onCaptionGenericMotionListener: View.OnGenericMotionListener, 81 mOnLeftSnapClickListener: () -> Unit, 82 mOnRightSnapClickListener: () -> Unit, 83 mOnMaximizeOrRestoreClickListener: () -> Unit, 84 onMaximizeHoverAnimationFinishedListener: () -> Unit, 85 private val desktopModeUiEventLogger: DesktopModeUiEventLogger, 86 ) : WindowDecorationViewHolder<AppHeaderViewHolder.HeaderData>(rootView) { 87 88 data class HeaderData( 89 val taskInfo: RunningTaskInfo, 90 val isTaskMaximized: Boolean, 91 val inFullImmersiveState: Boolean, 92 val hasGlobalFocus: Boolean, 93 val enableMaximizeLongClick: Boolean, 94 val isCaptionVisible: Boolean, 95 ) : Data() 96 97 private val decorThemeUtil = DecorThemeUtil(context) 98 private val lightColors = dynamicLightColorScheme(context) 99 private val darkColors = dynamicDarkColorScheme(context) 100 101 /** 102 * The corner radius to apply to the app chip, maximize and close button's background drawable. 103 **/ 104 private val headerButtonsRippleRadius = context.resources 105 .getDimensionPixelSize(R.dimen.desktop_mode_header_buttons_ripple_radius) 106 107 /** 108 * The app chip, minimize, maximize and close button's height extends to the top & bottom edges 109 * of the header, and their width may be larger than their height. This is by design to increase 110 * the clickable and hover-able bounds of the view as much as possible. However, to prevent the 111 * ripple drawable from being as large as the views (and asymmetrical), insets are applied to 112 * the background ripple drawable itself to give the appearance of a smaller button 113 * (with padding between itself and the header edges / sibling buttons) but without affecting 114 * its touchable region. 115 */ 116 private val appChipDrawableInsets = DrawableInsets( 117 vertical = context.resources 118 .getDimensionPixelSize(R.dimen.desktop_mode_header_app_chip_ripple_inset_vertical) 119 ) 120 private val minimizeDrawableInsets = DrawableInsets( 121 vertical = context.resources 122 .getDimensionPixelSize(R.dimen.desktop_mode_header_minimize_ripple_inset_vertical), 123 horizontal = context.resources 124 .getDimensionPixelSize(R.dimen.desktop_mode_header_minimize_ripple_inset_horizontal) 125 ) 126 private val maximizeDrawableInsets = DrawableInsets( 127 vertical = context.resources 128 .getDimensionPixelSize(R.dimen.desktop_mode_header_maximize_ripple_inset_vertical), 129 horizontal = context.resources 130 .getDimensionPixelSize(R.dimen.desktop_mode_header_maximize_ripple_inset_horizontal) 131 ) 132 private val closeDrawableInsets = DrawableInsets( 133 vertical = context.resources 134 .getDimensionPixelSize(R.dimen.desktop_mode_header_close_ripple_inset_vertical), 135 horizontal = context.resources 136 .getDimensionPixelSize(R.dimen.desktop_mode_header_close_ripple_inset_horizontal) 137 ) 138 139 private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) 140 private val captionHandle: View = rootView.requireViewById(R.id.caption_handle) 141 private val openMenuButton: View = rootView.requireViewById(R.id.open_menu_button) 142 private val closeWindowButton: ImageButton = rootView.requireViewById(R.id.close_window) 143 private val expandMenuButton: ImageButton = rootView.requireViewById(R.id.expand_menu_button) 144 private val maximizeButtonView: MaximizeButtonView = 145 rootView.requireViewById(R.id.maximize_button_view) 146 private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window) 147 private val minimizeWindowButton: ImageButton = rootView.requireViewById(R.id.minimize_window) 148 private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name) 149 private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon) 150 val appNameTextWidth: Int 151 get() = appNameTextView.width 152 153 private val a11yAnnounceTextMaximize: String = 154 context.getString(R.string.app_header_talkback_action_maximize_button_text) 155 private val a11yAnnounceTextRestore: String = 156 context.getString(R.string.app_header_talkback_action_restore_button_text) 157 158 private lateinit var sizeToggleDirection: SizeToggleDirection 159 private lateinit var a11yTextMaximize: String 160 private lateinit var a11yTextRestore: String 161 162 private lateinit var currentTaskInfo: RunningTaskInfo 163 164 init { 165 captionView.setOnTouchListener(onCaptionTouchListener) 166 captionHandle.setOnTouchListener(onCaptionTouchListener) 167 openMenuButton.setOnClickListener(onCaptionButtonClickListener) 168 openMenuButton.setOnTouchListener(onCaptionTouchListener) 169 closeWindowButton.setOnClickListener(onCaptionButtonClickListener) 170 maximizeWindowButton.setOnClickListener(onCaptionButtonClickListener) 171 maximizeWindowButton.setOnTouchListener(onCaptionTouchListener) 172 maximizeWindowButton.setOnGenericMotionListener(onCaptionGenericMotionListener) 173 maximizeWindowButton.onLongClickListener = onLongClickListener 174 closeWindowButton.setOnTouchListener(onCaptionTouchListener) 175 minimizeWindowButton.setOnClickListener(onCaptionButtonClickListener) 176 minimizeWindowButton.setOnTouchListener(onCaptionTouchListener) 177 maximizeButtonView.onHoverAnimationFinishedListener = 178 onMaximizeHoverAnimationFinishedListener 179 180 val a11yActionSnapLeft = AccessibilityAction( 181 R.id.action_snap_left, 182 context.getString(R.string.desktop_mode_a11y_action_snap_left) 183 ) 184 val a11yActionSnapRight = AccessibilityAction( 185 R.id.action_snap_right, 186 context.getString(R.string.desktop_mode_a11y_action_snap_right) 187 ) 188 val a11yActionMaximizeRestore = AccessibilityAction( 189 R.id.action_maximize_restore, 190 context.getString(R.string.desktop_mode_a11y_action_maximize_restore) 191 ) 192 193 captionHandle.accessibilityDelegate = object : View.AccessibilityDelegate() { onInitializeAccessibilityNodeInfonull194 override fun onInitializeAccessibilityNodeInfo( 195 host: View, 196 info: AccessibilityNodeInfo 197 ) { 198 super.onInitializeAccessibilityNodeInfo(host, info) 199 info.addAction(a11yActionSnapLeft) 200 info.addAction(a11yActionSnapRight) 201 info.addAction(a11yActionMaximizeRestore) 202 } 203 performAccessibilityActionnull204 override fun performAccessibilityAction( 205 host: View, 206 action: Int, 207 args: Bundle? 208 ): Boolean { 209 when (action) { 210 R.id.action_snap_left -> { 211 desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_RESIZE_LEFT) 212 mOnLeftSnapClickListener.invoke() 213 } 214 R.id.action_snap_right -> { 215 desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_RESIZE_RIGHT) 216 mOnRightSnapClickListener.invoke() 217 } 218 R.id.action_maximize_restore -> { 219 desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_MAXIMIZE_RESTORE) 220 mOnMaximizeOrRestoreClickListener.invoke() 221 } 222 } 223 224 return super.performAccessibilityAction(host, action, args) 225 } 226 } 227 maximizeWindowButton.accessibilityDelegate = object : View.AccessibilityDelegate() { onInitializeAccessibilityNodeInfonull228 override fun onInitializeAccessibilityNodeInfo( 229 host: View, 230 info: AccessibilityNodeInfo 231 ) { 232 super.onInitializeAccessibilityNodeInfo(host, info) 233 info.addAction(AccessibilityAction.ACTION_CLICK) 234 info.addAction(a11yActionSnapLeft) 235 info.addAction(a11yActionSnapRight) 236 info.addAction(a11yActionMaximizeRestore) 237 host.isClickable = true 238 } 239 performAccessibilityActionnull240 override fun performAccessibilityAction( 241 host: View, 242 action: Int, 243 args: Bundle? 244 ): Boolean { 245 when (action) { 246 AccessibilityAction.ACTION_CLICK.id -> { 247 desktopModeUiEventLogger.log( 248 currentTaskInfo, A11Y_APP_WINDOW_MAXIMIZE_RESTORE_BUTTON 249 ) 250 host.performClick() 251 } 252 R.id.action_snap_left -> { 253 desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_RESIZE_LEFT) 254 mOnLeftSnapClickListener.invoke() 255 } 256 R.id.action_snap_right -> { 257 desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_RESIZE_RIGHT) 258 mOnRightSnapClickListener.invoke() 259 } 260 R.id.action_maximize_restore -> { 261 desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_MAXIMIZE_RESTORE) 262 mOnMaximizeOrRestoreClickListener.invoke() 263 } 264 } 265 266 return super.performAccessibilityAction(host, action, args) 267 } 268 } 269 270 closeWindowButton.accessibilityDelegate = object : View.AccessibilityDelegate() { performAccessibilityActionnull271 override fun performAccessibilityAction( 272 host: View, 273 action: Int, 274 args: Bundle? 275 ): Boolean { 276 when (action) { 277 AccessibilityAction.ACTION_CLICK.id -> desktopModeUiEventLogger.log( 278 currentTaskInfo, A11Y_APP_WINDOW_CLOSE_BUTTON 279 ) 280 } 281 282 return super.performAccessibilityAction(host, action, args) 283 } 284 } 285 286 minimizeWindowButton.accessibilityDelegate = object : View.AccessibilityDelegate() { performAccessibilityActionnull287 override fun performAccessibilityAction( 288 host: View, 289 action: Int, 290 args: Bundle? 291 ): Boolean { 292 when (action) { 293 AccessibilityAction.ACTION_CLICK.id -> desktopModeUiEventLogger.log( 294 currentTaskInfo, A11Y_APP_WINDOW_MINIMIZE_BUTTON 295 ) 296 } 297 298 return super.performAccessibilityAction(host, action, args) 299 } 300 } 301 302 // Update a11y announcement to say "double tap to open menu" 303 ViewCompat.replaceAccessibilityAction( 304 openMenuButton, 305 AccessibilityActionCompat.ACTION_CLICK, 306 context.getString(R.string.app_handle_chip_accessibility_announce), 307 null 308 ) 309 310 // Update a11y announcement to say "double tap to minimize app window" 311 ViewCompat.replaceAccessibilityAction( 312 minimizeWindowButton, 313 AccessibilityActionCompat.ACTION_CLICK, 314 context.getString(R.string.app_header_talkback_action_minimize_button_text), 315 null 316 ) 317 318 // Update a11y announcement to say "double tap to close app window" 319 ViewCompat.replaceAccessibilityAction( 320 closeWindowButton, 321 AccessibilityActionCompat.ACTION_CLICK, 322 context.getString(R.string.app_header_talkback_action_close_button_text), 323 null 324 ) 325 } 326 bindDatanull327 override fun bindData(data: HeaderData) { 328 bindData( 329 data.taskInfo, 330 data.isTaskMaximized, 331 data.inFullImmersiveState, 332 data.hasGlobalFocus, 333 data.enableMaximizeLongClick, 334 data.isCaptionVisible, 335 ) 336 } 337 338 /** Sets the app's name in the header. */ setAppNamenull339 fun setAppName(name: CharSequence) { 340 appNameTextView.text = name 341 openMenuButton.contentDescription = 342 context.getString(R.string.desktop_mode_app_header_chip_text, name) 343 344 closeWindowButton.contentDescription = context.getString(R.string.close_button_text, name) 345 minimizeWindowButton.contentDescription = 346 context.getString(R.string.minimize_button_text, name) 347 348 a11yTextMaximize = context.getString(R.string.maximize_button_text, name) 349 a11yTextRestore = context.getString(R.string.restore_button_text, name) 350 351 updateMaximizeButtonContentDescription() 352 } 353 updateMaximizeButtonContentDescriptionnull354 private fun updateMaximizeButtonContentDescription() { 355 if (this::a11yTextRestore.isInitialized && 356 this::a11yTextMaximize.isInitialized && 357 this::sizeToggleDirection.isInitialized) { 358 maximizeWindowButton.contentDescription = when (sizeToggleDirection) { 359 SizeToggleDirection.MAXIMIZE -> a11yTextMaximize 360 SizeToggleDirection.RESTORE -> a11yTextRestore 361 } 362 } 363 } 364 365 /** Sets the app's icon in the header. */ setAppIconnull366 fun setAppIcon(icon: Bitmap) { 367 appIconImageView.setImageBitmap(icon) 368 } 369 bindDatanull370 private fun bindData( 371 taskInfo: RunningTaskInfo, 372 isTaskMaximized: Boolean, 373 inFullImmersiveState: Boolean, 374 hasGlobalFocus: Boolean, 375 enableMaximizeLongClick: Boolean, 376 isCaptionVisible: Boolean, 377 ) { 378 currentTaskInfo = taskInfo 379 if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue) { 380 bindDataWithThemedHeaders( 381 taskInfo, 382 isTaskMaximized, 383 inFullImmersiveState, 384 hasGlobalFocus, 385 enableMaximizeLongClick, 386 isCaptionVisible, 387 ) 388 } else { 389 bindDataLegacy(taskInfo, hasGlobalFocus, isCaptionVisible) 390 } 391 } 392 bindDataLegacynull393 private fun bindDataLegacy( 394 taskInfo: RunningTaskInfo, 395 hasGlobalFocus: Boolean, 396 isCaptionVisible: Boolean, 397 ) { 398 if (DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) { 399 setCaptionVisibility(isCaptionVisible) 400 } 401 captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo, hasGlobalFocus)) 402 val color = getAppNameAndButtonColor(taskInfo, hasGlobalFocus) 403 val alpha = Color.alpha(color) 404 closeWindowButton.imageTintList = ColorStateList.valueOf(color) 405 maximizeWindowButton.imageTintList = ColorStateList.valueOf(color) 406 minimizeWindowButton.imageTintList = ColorStateList.valueOf(color) 407 expandMenuButton.imageTintList = ColorStateList.valueOf(color) 408 appNameTextView.isVisible = !taskInfo.isTransparentCaptionBarAppearance 409 appNameTextView.setTextColor(color) 410 appIconImageView.imageAlpha = alpha 411 maximizeWindowButton.imageAlpha = alpha 412 minimizeWindowButton.imageAlpha = alpha 413 closeWindowButton.imageAlpha = alpha 414 expandMenuButton.imageAlpha = alpha 415 context.withStyledAttributes( 416 set = null, 417 attrs = intArrayOf( 418 android.R.attr.selectableItemBackground, 419 android.R.attr.selectableItemBackgroundBorderless 420 ), 421 defStyleAttr = 0, 422 defStyleRes = 0 423 ) { 424 openMenuButton.background = getDrawable(0) 425 maximizeWindowButton.background = getDrawable(1) 426 closeWindowButton.background = getDrawable(1) 427 minimizeWindowButton.background = getDrawable(1) 428 } 429 maximizeButtonView.setAnimationTints(isDarkMode()) 430 minimizeWindowButton.isGone = !DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue 431 } 432 bindDataWithThemedHeadersnull433 private fun bindDataWithThemedHeaders( 434 taskInfo: RunningTaskInfo, 435 isTaskMaximized: Boolean, 436 inFullImmersiveState: Boolean, 437 hasGlobalFocus: Boolean, 438 enableMaximizeLongClick: Boolean, 439 isCaptionVisible: Boolean, 440 ) { 441 val header = fillHeaderInfo(taskInfo, hasGlobalFocus) 442 val headerStyle = getHeaderStyle(header) 443 444 if (DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) { 445 setCaptionVisibility(isCaptionVisible) 446 } 447 448 // Caption Background 449 when (headerStyle.background) { 450 is HeaderStyle.Background.Opaque -> { 451 captionView.setBackgroundColor(headerStyle.background.color) 452 } 453 HeaderStyle.Background.Transparent -> { 454 captionView.setBackgroundColor(Color.TRANSPARENT) 455 } 456 } 457 458 // Caption Foreground 459 val foregroundColor = headerStyle.foreground.color 460 val foregroundAlpha = headerStyle.foreground.opacity 461 val colorStateList = ColorStateList.valueOf(foregroundColor).withAlpha(foregroundAlpha) 462 // App chip. 463 openMenuButton.apply { 464 background = createBackgroundDrawable( 465 color = foregroundColor, 466 cornerRadius = headerButtonsRippleRadius, 467 drawableInsets = appChipDrawableInsets, 468 ) 469 expandMenuButton.imageTintList = colorStateList 470 appNameTextView.apply { 471 isVisible = header.type == Header.Type.DEFAULT 472 setTextColor(colorStateList) 473 } 474 appIconImageView.imageAlpha = foregroundAlpha 475 defaultFocusHighlightEnabled = false 476 } 477 // Minimize button. 478 minimizeWindowButton.apply { 479 imageTintList = colorStateList 480 background = createBackgroundDrawable( 481 color = foregroundColor, 482 cornerRadius = headerButtonsRippleRadius, 483 drawableInsets = minimizeDrawableInsets 484 ) 485 } 486 minimizeWindowButton.isGone = !DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue 487 // Maximize button. 488 maximizeButtonView.apply { 489 setAnimationTints( 490 darkMode = header.appTheme == Theme.DARK, 491 iconForegroundColor = colorStateList, 492 baseForegroundColor = foregroundColor, 493 backgroundDrawable = createBackgroundDrawable( 494 color = foregroundColor, 495 cornerRadius = headerButtonsRippleRadius, 496 drawableInsets = maximizeDrawableInsets 497 ) 498 ) 499 val icon = getMaximizeButtonIcon(isTaskMaximized, inFullImmersiveState) 500 setIcon(icon) 501 502 when (icon) { 503 R.drawable.decor_desktop_mode_immersive_or_maximize_exit_button_dark -> { 504 sizeToggleDirection = SizeToggleDirection.RESTORE 505 506 // Update a11y announcement to say "double tap to maximize app window size" 507 ViewCompat.replaceAccessibilityAction( 508 maximizeWindowButton, 509 AccessibilityActionCompat.ACTION_CLICK, 510 a11yAnnounceTextRestore, 511 null 512 ) 513 } 514 R.drawable.decor_desktop_mode_maximize_button_dark -> { 515 sizeToggleDirection = SizeToggleDirection.MAXIMIZE 516 517 // Update a11y announcement to say "double tap to restore app window size" 518 ViewCompat.replaceAccessibilityAction( 519 maximizeWindowButton, 520 AccessibilityActionCompat.ACTION_CLICK, 521 a11yAnnounceTextMaximize, 522 null 523 ) 524 } 525 } 526 updateMaximizeButtonContentDescription() 527 } 528 // Close button. 529 closeWindowButton.apply { 530 imageTintList = colorStateList 531 background = createBackgroundDrawable( 532 color = foregroundColor, 533 cornerRadius = headerButtonsRippleRadius, 534 drawableInsets = closeDrawableInsets 535 ) 536 } 537 if (!enableMaximizeLongClick) { 538 maximizeButtonView.cancelHoverAnimation() 539 } 540 maximizeButtonView.hoverDisabled = !enableMaximizeLongClick 541 maximizeWindowButton.onLongClickListener = if (enableMaximizeLongClick) { 542 onLongClickListener 543 } else { 544 // Disable long-click to open maximize menu when in immersive. 545 null 546 } 547 } 548 setCaptionVisibilitynull549 private fun setCaptionVisibility(visible: Boolean) { 550 val v = if (visible) View.VISIBLE else View.GONE 551 captionView.visibility = v 552 } 553 onHandleMenuOpenednull554 override fun onHandleMenuOpened() {} 555 onHandleMenuClosednull556 override fun onHandleMenuClosed() {} 557 onMaximizeWindowHoverExitnull558 fun onMaximizeWindowHoverExit() { 559 maximizeButtonView.cancelHoverAnimation() 560 } 561 onMaximizeWindowHoverEnternull562 fun onMaximizeWindowHoverEnter() { 563 maximizeButtonView.startHoverAnimation() 564 } 565 runOnAppChipGlobalLayoutnull566 fun runOnAppChipGlobalLayout(runnable: () -> Unit) { 567 // Wait for app chip to be inflated before notifying repository. 568 openMenuButton.viewTreeObserver.addOnGlobalLayoutListener(object : 569 OnGlobalLayoutListener { 570 override fun onGlobalLayout() { 571 runnable() 572 openMenuButton.viewTreeObserver.removeOnGlobalLayoutListener(this) 573 } 574 }) 575 } 576 getAppChipLocationInWindownull577 fun getAppChipLocationInWindow(): Rect { 578 val appChipBoundsInWindow = IntArray(2) 579 openMenuButton.getLocationInWindow(appChipBoundsInWindow) 580 581 return Rect( 582 /* left = */ appChipBoundsInWindow[0], 583 /* top = */ appChipBoundsInWindow[1], 584 /* right = */ appChipBoundsInWindow[0] + openMenuButton.width, 585 /* bottom = */ appChipBoundsInWindow[1] + openMenuButton.height 586 ) 587 } 588 requestAccessibilityFocusnull589 fun requestAccessibilityFocus() { 590 maximizeWindowButton.post { 591 maximizeWindowButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) 592 } 593 } 594 595 @DrawableRes getMaximizeButtonIconnull596 private fun getMaximizeButtonIcon( 597 isTaskMaximized: Boolean, 598 inFullImmersiveState: Boolean 599 ): Int = when { 600 shouldShowExitFullImmersiveOrMaximizeIcon(isTaskMaximized, inFullImmersiveState) -> { 601 R.drawable.decor_desktop_mode_immersive_or_maximize_exit_button_dark 602 } 603 else -> R.drawable.decor_desktop_mode_maximize_button_dark 604 } 605 shouldShowExitFullImmersiveOrMaximizeIconnull606 private fun shouldShowExitFullImmersiveOrMaximizeIcon( 607 isTaskMaximized: Boolean, 608 inFullImmersiveState: Boolean 609 ): Boolean = (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue && inFullImmersiveState) 610 || isTaskMaximized 611 612 private fun getHeaderStyle(header: Header): HeaderStyle { 613 return HeaderStyle( 614 background = getHeaderBackground(header), 615 foreground = getHeaderForeground(header) 616 ) 617 } 618 getHeaderBackgroundnull619 private fun getHeaderBackground(header: Header): HeaderStyle.Background { 620 return when (header.type) { 621 Header.Type.DEFAULT -> { 622 when (header.appTheme) { 623 Theme.LIGHT -> { 624 if (header.isFocused) { 625 HeaderStyle.Background.Opaque(lightColors.secondaryContainer.toArgb()) 626 } else { 627 HeaderStyle.Background.Opaque(lightColors.surfaceContainerLow.toArgb()) 628 } 629 } 630 Theme.DARK -> { 631 if (header.isFocused) { 632 HeaderStyle.Background.Opaque(darkColors.surfaceContainerHigh.toArgb()) 633 } else { 634 HeaderStyle.Background.Opaque(darkColors.surfaceDim.toArgb()) 635 } 636 } 637 } 638 } 639 Header.Type.CUSTOM -> HeaderStyle.Background.Transparent 640 } 641 } 642 getHeaderForegroundnull643 private fun getHeaderForeground(header: Header): HeaderStyle.Foreground { 644 return when (header.type) { 645 Header.Type.DEFAULT -> { 646 when (header.appTheme) { 647 Theme.LIGHT -> { 648 if (header.isFocused) { 649 HeaderStyle.Foreground( 650 color = lightColors.onSecondaryContainer.toArgb(), 651 opacity = OPACITY_100 652 ) 653 } else { 654 HeaderStyle.Foreground( 655 color = lightColors.onSecondaryContainer.toArgb(), 656 opacity = OPACITY_65 657 ) 658 } 659 } 660 Theme.DARK -> { 661 if (header.isFocused) { 662 HeaderStyle.Foreground( 663 color = darkColors.onSurface.toArgb(), 664 opacity = OPACITY_100 665 ) 666 } else { 667 HeaderStyle.Foreground( 668 color = darkColors.onSurface.toArgb(), 669 opacity = OPACITY_55 670 ) 671 } 672 } 673 } 674 } 675 Header.Type.CUSTOM -> when { 676 header.isAppearanceCaptionLight && header.isFocused -> { 677 HeaderStyle.Foreground( 678 color = lightColors.onSecondaryContainer.toArgb(), 679 opacity = OPACITY_100 680 ) 681 } 682 header.isAppearanceCaptionLight && !header.isFocused -> { 683 HeaderStyle.Foreground( 684 color = lightColors.onSecondaryContainer.toArgb(), 685 opacity = OPACITY_65 686 ) 687 } 688 !header.isAppearanceCaptionLight && header.isFocused -> { 689 HeaderStyle.Foreground( 690 color = darkColors.onSurface.toArgb(), 691 opacity = OPACITY_100 692 ) 693 } 694 !header.isAppearanceCaptionLight && !header.isFocused -> { 695 HeaderStyle.Foreground( 696 color = darkColors.onSurface.toArgb(), 697 opacity = OPACITY_55 698 ) 699 } 700 else -> error("No other combination expected header=$header") 701 } 702 } 703 } 704 fillHeaderInfonull705 private fun fillHeaderInfo(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean): Header { 706 return Header( 707 type = if (taskInfo.isTransparentCaptionBarAppearance) { 708 Header.Type.CUSTOM 709 } else { 710 Header.Type.DEFAULT 711 }, 712 appTheme = decorThemeUtil.getAppTheme(taskInfo), 713 isFocused = hasGlobalFocus, 714 isAppearanceCaptionLight = taskInfo.isLightCaptionBarAppearance 715 ) 716 } 717 718 private enum class SizeToggleDirection { 719 MAXIMIZE, RESTORE 720 } 721 722 private data class Header( 723 val type: Type, 724 val appTheme: Theme, 725 val isFocused: Boolean, 726 val isAppearanceCaptionLight: Boolean, 727 ) { 728 enum class Type { DEFAULT, CUSTOM } 729 } 730 731 private data class HeaderStyle( 732 val background: Background, 733 val foreground: Foreground 734 ) { 735 data class Foreground( 736 @ColorInt val color: Int, 737 val opacity: Int 738 ) 739 740 sealed class Background { 741 data object Transparent : Background() 742 data class Opaque(@ColorInt val color: Int) : Background() 743 } 744 } 745 746 @ColorInt getCaptionBackgroundColornull747 private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean): Int { 748 if (taskInfo.isTransparentCaptionBarAppearance) { 749 return Color.TRANSPARENT 750 } 751 val materialColorAttr: Int = 752 if (isDarkMode()) { 753 if (!hasGlobalFocus) { 754 materialColorSurfaceContainerHigh 755 } else { 756 materialColorSurfaceDim 757 } 758 } else { 759 if (!hasGlobalFocus) { 760 materialColorSurfaceContainerLow 761 } else { 762 materialColorSecondaryContainer 763 } 764 } 765 context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) { 766 return getColor(0, 0) 767 } 768 return 0 769 } 770 771 @ColorInt getAppNameAndButtonColornull772 private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean): Int { 773 val materialColor = context.getColor(when { 774 taskInfo.isTransparentCaptionBarAppearance && 775 taskInfo.isLightCaptionBarAppearance -> materialColorOnSecondaryContainer 776 taskInfo.isTransparentCaptionBarAppearance && 777 !taskInfo.isLightCaptionBarAppearance -> materialColorOnSurface 778 isDarkMode() -> materialColorOnSurface 779 else -> materialColorOnSecondaryContainer 780 }) 781 val appDetailsOpacity = when { 782 isDarkMode() && !hasGlobalFocus -> DARK_THEME_UNFOCUSED_OPACITY 783 !isDarkMode() && !hasGlobalFocus -> LIGHT_THEME_UNFOCUSED_OPACITY 784 else -> FOCUSED_OPACITY 785 } 786 787 788 return if (appDetailsOpacity == FOCUSED_OPACITY) { 789 materialColor 790 } else { 791 Color.argb( 792 appDetailsOpacity, 793 Color.red(materialColor), 794 Color.green(materialColor), 795 Color.blue(materialColor) 796 ) 797 } 798 } 799 isDarkModenull800 private fun isDarkMode(): Boolean { 801 return context.resources.configuration.uiMode and 802 Configuration.UI_MODE_NIGHT_MASK == 803 Configuration.UI_MODE_NIGHT_YES 804 } 805 closenull806 override fun close() { 807 // Should not fire long press events after closing the window decoration. 808 maximizeWindowButton.cancelLongPress() 809 } 810 811 companion object { 812 private const val TAG = "DesktopModeAppControlsWindowDecorationViewHolder" 813 814 private const val DARK_THEME_UNFOCUSED_OPACITY = 140 // 55% 815 private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65% 816 private const val FOCUSED_OPACITY = 255 817 } 818 819 class Factory { createnull820 fun create( 821 rootView: View, 822 onCaptionTouchListener: View.OnTouchListener, 823 onCaptionButtonClickListener: View.OnClickListener, 824 onLongClickListener: OnLongClickListener, 825 onCaptionGenericMotionListener: View.OnGenericMotionListener, 826 mOnLeftSnapClickListener: () -> Unit, 827 mOnRightSnapClickListener: () -> Unit, 828 mOnMaximizeOrRestoreClickListener: () -> Unit, 829 onMaximizeHoverAnimationFinishedListener: () -> Unit, 830 desktopModeUiEventLogger: DesktopModeUiEventLogger 831 ): AppHeaderViewHolder = AppHeaderViewHolder( 832 rootView, 833 onCaptionTouchListener, 834 onCaptionButtonClickListener, 835 onLongClickListener, 836 onCaptionGenericMotionListener, 837 mOnLeftSnapClickListener, 838 mOnRightSnapClickListener, 839 mOnMaximizeOrRestoreClickListener, 840 onMaximizeHoverAnimationFinishedListener, 841 desktopModeUiEventLogger, 842 ) 843 } 844 } 845