• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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