• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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 
17 package com.android.wm.shell.windowdecor
18 
19 import android.animation.AnimatorSet
20 import android.animation.ObjectAnimator
21 import android.animation.ValueAnimator
22 import android.annotation.ColorInt
23 import android.app.ActivityManager.RunningTaskInfo
24 import android.content.Context
25 import android.content.res.ColorStateList
26 import android.content.res.Resources
27 import android.graphics.Paint
28 import android.graphics.PixelFormat
29 import android.graphics.Point
30 import android.graphics.Rect
31 import android.graphics.drawable.Drawable
32 import android.graphics.drawable.GradientDrawable
33 import android.graphics.drawable.LayerDrawable
34 import android.graphics.drawable.ShapeDrawable
35 import android.graphics.drawable.StateListDrawable
36 import android.graphics.drawable.shapes.RoundRectShape
37 import android.os.Bundle
38 import android.util.StateSet
39 import android.view.LayoutInflater
40 import android.view.MotionEvent.ACTION_HOVER_ENTER
41 import android.view.MotionEvent.ACTION_HOVER_EXIT
42 import android.view.MotionEvent.ACTION_HOVER_MOVE
43 import android.view.MotionEvent.ACTION_OUTSIDE
44 import android.view.SurfaceControl
45 import android.view.SurfaceControl.Transaction
46 import android.view.SurfaceControlViewHost
47 import android.view.View
48 import android.view.View.SCALE_Y
49 import android.view.View.TRANSLATION_Y
50 import android.view.View.TRANSLATION_Z
51 import android.view.ViewGroup
52 import android.view.WindowManager
53 import android.view.WindowlessWindowManager
54 import android.view.accessibility.AccessibilityEvent
55 import android.view.accessibility.AccessibilityNodeInfo
56 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
57 import android.widget.Button
58 import android.widget.TextView
59 import android.window.TaskConstants
60 import androidx.compose.material3.ColorScheme
61 import androidx.compose.ui.graphics.toArgb
62 import androidx.core.animation.addListener
63 import androidx.core.view.isGone
64 import androidx.core.view.isVisible
65 import com.android.wm.shell.R
66 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
67 import com.android.wm.shell.common.DisplayController
68 import com.android.wm.shell.common.SyncTransactionQueue
69 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger
70 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_MAXIMIZE_MENU_MAXIMIZE
71 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_MAXIMIZE_MENU_RESIZE_LEFT
72 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_MAXIMIZE_MENU_RESIZE_RIGHT
73 import com.android.wm.shell.desktopmode.isTaskMaximized
74 import com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED_DECELERATE
75 import com.android.wm.shell.shared.animation.Interpolators.FAST_OUT_LINEAR_IN
76 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer
77 import com.android.wm.shell.windowdecor.common.DecorThemeUtil
78 import com.android.wm.shell.windowdecor.common.OPACITY_12
79 import com.android.wm.shell.windowdecor.common.OPACITY_40
80 import com.android.wm.shell.windowdecor.common.OPACITY_60
81 import com.android.wm.shell.windowdecor.common.withAlpha
82 import java.util.function.Supplier
83 
84 /**
85  *  Menu that appears when user long clicks the maximize button. Gives the user the option to
86  *  maximize the task or restore previous task bounds from the maximized state and to snap the task
87  *  to the right or left half of the screen.
88  */
89 class MaximizeMenu(
90     private val syncQueue: SyncTransactionQueue,
91     private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
92     private val displayController: DisplayController,
93     private val taskInfo: RunningTaskInfo,
94     private val decorWindowContext: Context,
95     private val positionSupplier: (Int, Int) -> Point,
96     private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
97     private val desktopModeUiEventLogger: DesktopModeUiEventLogger
98 ) {
99     private var maximizeMenu: AdditionalViewHostViewContainer? = null
100     private var maximizeMenuView: MaximizeMenuView? = null
101     private lateinit var viewHost: SurfaceControlViewHost
102     private lateinit var leash: SurfaceControl
103     private val cornerRadius = loadDimensionPixelSize(
104             R.dimen.desktop_mode_maximize_menu_corner_radius
105     ).toFloat()
106     private lateinit var menuPosition: Point
107     private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding)
108 
109     /** Position the menu relative to the caption's position. */
positionMenunull110     fun positionMenu(t: Transaction) {
111         menuPosition = positionSupplier(maximizeMenuView?.measureWidth() ?: 0,
112                                         maximizeMenuView?.measureHeight() ?: 0)
113         t.setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat())
114     }
115 
116     /** Creates and shows the maximize window. */
shownull117     fun show(
118         isTaskInImmersiveMode: Boolean,
119         showImmersiveOption: Boolean,
120         showSnapOptions: Boolean,
121         onMaximizeOrRestoreClickListener: () -> Unit,
122         onImmersiveOrRestoreClickListener: () -> Unit,
123         onLeftSnapClickListener: () -> Unit,
124         onRightSnapClickListener: () -> Unit,
125         onHoverListener: (Boolean) -> Unit,
126         onOutsideTouchListener: () -> Unit,
127     ) {
128         if (maximizeMenu != null) return
129         createMaximizeMenu(
130             isTaskInImmersiveMode = isTaskInImmersiveMode,
131             showImmersiveOption = showImmersiveOption,
132             showSnapOptions = showSnapOptions,
133             onMaximizeClickListener = onMaximizeOrRestoreClickListener,
134             onImmersiveOrRestoreClickListener = onImmersiveOrRestoreClickListener,
135             onLeftSnapClickListener = onLeftSnapClickListener,
136             onRightSnapClickListener = onRightSnapClickListener,
137             onHoverListener = onHoverListener,
138             onOutsideTouchListener = onOutsideTouchListener,
139         )
140         maximizeMenuView?.let { view ->
141             view.animateOpenMenu(onEnd = {
142                 view.requestAccessibilityFocus()
143             })
144         }
145     }
146 
147     /** Closes the maximize window and releases its view. */
closenull148     fun close(onEnd: () -> Unit) {
149         val view = maximizeMenuView
150         val menu = maximizeMenu
151         if (view == null) {
152             menu?.releaseView()
153         } else {
154             view.animateCloseMenu(onEnd = {
155                 menu?.releaseView()
156                 onEnd.invoke()
157             })
158         }
159         maximizeMenu = null
160         maximizeMenuView = null
161     }
162 
163     /** Create a maximize menu that is attached to the display area. */
createMaximizeMenunull164     private fun createMaximizeMenu(
165         isTaskInImmersiveMode: Boolean,
166         showImmersiveOption: Boolean,
167         showSnapOptions: Boolean,
168         onMaximizeClickListener: () -> Unit,
169         onImmersiveOrRestoreClickListener: () -> Unit,
170         onLeftSnapClickListener: () -> Unit,
171         onRightSnapClickListener: () -> Unit,
172         onHoverListener: (Boolean) -> Unit,
173         onOutsideTouchListener: () -> Unit,
174     ) {
175         val t = transactionSupplier.get()
176         val builder = SurfaceControl.Builder()
177         rootTdaOrganizer.attachToDisplayArea(taskInfo.displayId, builder)
178         leash = builder
179                 .setName("Maximize Menu")
180                 .setContainerLayer()
181                 .build()
182         val windowManager = WindowlessWindowManager(
183                 taskInfo.configuration,
184                 leash,
185                 null // HostInputToken
186         )
187         viewHost = SurfaceControlViewHost(decorWindowContext,
188                 displayController.getDisplay(taskInfo.displayId), windowManager,
189                 "MaximizeMenu")
190         maximizeMenuView = MaximizeMenuView(
191             context = decorWindowContext,
192             desktopModeUiEventLogger = desktopModeUiEventLogger,
193             sizeToggleDirection = getSizeToggleDirection(),
194             immersiveConfig = if (showImmersiveOption) {
195                 MaximizeMenuView.ImmersiveConfig.Visible(
196                     getImmersiveToggleDirection(isTaskInImmersiveMode)
197                 )
198             } else {
199                 MaximizeMenuView.ImmersiveConfig.Hidden
200             },
201             showSnapOptions = showSnapOptions,
202             menuPadding = menuPadding,
203         ).also { menuView ->
204             menuView.bind(taskInfo)
205             menuView.onMaximizeClickListener = onMaximizeClickListener
206             menuView.onImmersiveOrRestoreClickListener = onImmersiveOrRestoreClickListener
207             menuView.onLeftSnapClickListener = onLeftSnapClickListener
208             menuView.onRightSnapClickListener = onRightSnapClickListener
209             menuView.onMenuHoverListener = onHoverListener
210             menuView.onOutsideTouchListener = onOutsideTouchListener
211             val menuWidth = menuView.measureWidth()
212             val menuHeight = menuView.measureHeight()
213             menuPosition = positionSupplier(menuWidth, menuHeight)
214             val lp = WindowManager.LayoutParams(
215                     menuWidth,
216                     menuHeight,
217                     WindowManager.LayoutParams.TYPE_APPLICATION,
218                     WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
219                             or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
220                     PixelFormat.TRANSPARENT
221             )
222             lp.title = "Maximize Menu for Task=" + taskInfo.taskId
223             lp.setTrustedOverlay()
224             viewHost.setView(menuView.rootView, lp)
225         }
226 
227         // Bring menu to front when open
228         t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU)
229                 .setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat())
230                 .setCornerRadius(leash, cornerRadius)
231                 .show(leash)
232         maximizeMenu =
233             AdditionalViewHostViewContainer(leash, viewHost, transactionSupplier)
234 
235         syncQueue.runInSync { transaction ->
236             transaction.merge(t)
237             t.close()
238         }
239     }
240 
getSizeToggleDirectionnull241     private fun getSizeToggleDirection(): MaximizeMenuView.SizeToggleDirection {
242         val maximized = isTaskMaximized(taskInfo, displayController)
243         return if (maximized)
244             MaximizeMenuView.SizeToggleDirection.RESTORE
245         else
246             MaximizeMenuView.SizeToggleDirection.MAXIMIZE
247     }
248 
getImmersiveToggleDirectionnull249     private fun getImmersiveToggleDirection(
250         isTaskImmersive: Boolean
251     ): MaximizeMenuView.ImmersiveToggleDirection =
252         if (isTaskImmersive) {
253             MaximizeMenuView.ImmersiveToggleDirection.EXIT
254         } else {
255             MaximizeMenuView.ImmersiveToggleDirection.ENTER
256         }
257 
loadDimensionPixelSizenull258     private fun loadDimensionPixelSize(resourceId: Int): Int {
259         return if (resourceId == Resources.ID_NULL) {
260             0
261         } else {
262             decorWindowContext.resources.getDimensionPixelSize(resourceId)
263         }
264     }
265 
266     /**
267      * The view within the Maximize Menu, presents maximize, restore and snap-to-side options for
268      * resizing a Task.
269      */
270     class MaximizeMenuView(
271         context: Context,
272         private val desktopModeUiEventLogger: DesktopModeUiEventLogger,
273         private val sizeToggleDirection: SizeToggleDirection,
274         immersiveConfig: ImmersiveConfig,
275         showSnapOptions: Boolean,
276         private val menuPadding: Int
277     ) {
278         val rootView = LayoutInflater.from(context)
279             .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) as ViewGroup
280         private val container = requireViewById(R.id.container)
281         private val overlay = requireViewById(R.id.maximize_menu_overlay)
282         private val immersiveToggleContainer =
283             requireViewById(R.id.maximize_menu_immersive_toggle_container) as View
284         private val immersiveToggleButtonText =
285             requireViewById(R.id.maximize_menu_immersive_toggle_button_text) as TextView
286         private val immersiveToggleButton =
287             requireViewById(R.id.maximize_menu_immersive_toggle_button) as Button
288         private val sizeToggleContainer =
289             requireViewById(R.id.maximize_menu_size_toggle_container) as View
290         private val sizeToggleButtonText =
291             requireViewById(R.id.maximize_menu_size_toggle_button_text) as TextView
292         private val sizeToggleButton =
293             requireViewById(R.id.maximize_menu_size_toggle_button) as Button
294         private val snapContainer =
295             requireViewById(R.id.maximize_menu_snap_container) as View
296         private val snapWindowText =
297             requireViewById(R.id.maximize_menu_snap_window_text) as TextView
298         private val snapButtonsLayout =
299             requireViewById(R.id.maximize_menu_snap_menu_layout)
300 
301         // If layout direction is RTL, maximize menu will be mirrored, switching the order of the
302         // snap right/left buttons.
303         val isRtl: Boolean =
304             (context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL)
305         private val snapRightButton = if (isRtl) {
306             requireViewById(R.id.maximize_menu_snap_left_button) as Button
307         } else {
308             requireViewById(R.id.maximize_menu_snap_right_button) as Button
309         }
310         private val snapLeftButton = if (isRtl) {
311             requireViewById(R.id.maximize_menu_snap_right_button) as Button
312         } else {
313             requireViewById(R.id.maximize_menu_snap_left_button) as Button
314         }
315 
316         private val decorThemeUtil = DecorThemeUtil(context)
317 
318         private val outlineRadius = context.resources
319             .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_radius)
320         private val outlineStroke = context.resources
321             .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_stroke)
322         private val fillRadius = context.resources
323             .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius)
324 
325         private val immersiveFillPadding = context.resources.getDimensionPixelSize(R.dimen
326             .desktop_mode_maximize_menu_immersive_button_fill_padding)
327         private val maximizeFillPaddingDefault = context.resources.getDimensionPixelSize(R.dimen
328             .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding)
329         private val maximizeRestoreFillPaddingVertical = context.resources.getDimensionPixelSize(
330             R.dimen.desktop_mode_maximize_menu_restore_button_fill_vertical_padding)
331         private val maximizeRestoreFillPaddingHorizontal = context.resources.getDimensionPixelSize(
332             R.dimen.desktop_mode_maximize_menu_restore_button_fill_horizontal_padding)
333         private val maximizeFillPaddingRect = Rect(
334             maximizeFillPaddingDefault,
335             maximizeFillPaddingDefault,
336             maximizeFillPaddingDefault,
337             maximizeFillPaddingDefault
338         )
339         private val maximizeRestoreFillPaddingRect = Rect(
340             maximizeRestoreFillPaddingHorizontal,
341             maximizeRestoreFillPaddingVertical,
342             maximizeRestoreFillPaddingHorizontal,
343             maximizeRestoreFillPaddingVertical,
344         )
345         private val immersiveFillPaddingRect = Rect(
346             immersiveFillPadding,
347             immersiveFillPadding,
348             immersiveFillPadding,
349             immersiveFillPadding
350         )
351 
352         private val hoverTempRect = Rect()
353         private var menuAnimatorSet: AnimatorSet? = null
354         private lateinit var taskInfo: RunningTaskInfo
355         private lateinit var style: MenuStyle
356 
357         /** Invoked when the maximize or restore option is clicked. */
358         var onMaximizeClickListener: (() -> Unit)? = null
359         /** Invoked when the immersive or restore option is clicked. */
360         var onImmersiveOrRestoreClickListener: (() -> Unit)? = null
361         /** Invoked when the left snap option is clicked. */
362         var onLeftSnapClickListener: (() -> Unit)? = null
363         /** Invoked when the right snap option is clicked. */
364         var onRightSnapClickListener: (() -> Unit)? = null
365         /** Invoked whenever the hover state of the menu changes. */
366         var onMenuHoverListener: ((Boolean) -> Unit)? = null
367         /** Invoked whenever a click occurs outside the menu */
368         var onOutsideTouchListener: (() -> Unit)? = null
369 
370         init {
eventnull371             overlay.setOnHoverListener { _, event ->
372                 // The overlay covers the entire menu, so it's a convenient way to monitor whether
373                 // the menu is hovered as a whole or not.
374                 when (event.action) {
375                     ACTION_HOVER_ENTER -> onMenuHoverListener?.invoke(true)
376                     ACTION_HOVER_EXIT -> onMenuHoverListener?.invoke(false)
377                 }
378 
379                 // Also check if the hover falls within the snap options layout, to manually
380                 // set the left/right state based on the event's position.
381                 // TODO(b/346440693): this manual hover tracking is needed for left/right snap
382                 //  because its view/background(s) don't support selector states. Look into whether
383                 //  that can be added to avoid manual tracking. Also because these button
384                 //  colors/state logic is only being applied on hover events, but there's pressed,
385                 //  focused and selected states that should be responsive too.
386                 val snapLayoutBoundsRelToOverlay = hoverTempRect.also { rect ->
387                     snapButtonsLayout.getDrawingRect(rect)
388                     rootView.offsetDescendantRectToMyCoords(snapButtonsLayout, rect)
389                 }
390                 if (event.action == ACTION_HOVER_ENTER || event.action == ACTION_HOVER_MOVE) {
391                     if (snapLayoutBoundsRelToOverlay.contains(event.x.toInt(), event.y.toInt())) {
392                         // Hover is inside the snap layout, anything left of center is the left
393                         // snap, and anything right of center is right snap.
394                         val layoutCenter = snapLayoutBoundsRelToOverlay.centerX()
395                         if (event.x < layoutCenter) {
396                             updateSplitSnapSelection(SnapToHalfSelection.LEFT)
397                         } else {
398                             updateSplitSnapSelection(SnapToHalfSelection.RIGHT)
399                         }
400                     } else {
401                         // Any other hover is outside the snap layout, so neither is selected.
402                         updateSplitSnapSelection(SnapToHalfSelection.NONE)
403                     }
404                 }
405 
406                 // Don't consume the event to allow child views to receive the event too.
407                 return@setOnHoverListener false
408             }
409 
410             immersiveToggleContainer.isGone = immersiveConfig is ImmersiveConfig.Hidden
411             sizeToggleContainer.isVisible = true
412             snapContainer.isGone = !showSnapOptions
413 
<lambda>null414             immersiveToggleButton.setOnClickListener { onImmersiveOrRestoreClickListener?.invoke() }
<lambda>null415             sizeToggleButton.setOnClickListener { onMaximizeClickListener?.invoke() }
<lambda>null416             snapRightButton.setOnClickListener { onRightSnapClickListener?.invoke() }
<lambda>null417             snapLeftButton.setOnClickListener { onLeftSnapClickListener?.invoke() }
eventnull418             rootView.setOnTouchListener { _, event ->
419                 if (event.actionMasked == ACTION_OUTSIDE) {
420                     onOutsideTouchListener?.invoke()
421                     return@setOnTouchListener false
422                 }
423                 true
424             }
425 
426             sizeToggleButton.accessibilityDelegate = object : View.AccessibilityDelegate() {
onInitializeAccessibilityNodeInfonull427                 override fun onInitializeAccessibilityNodeInfo(
428                     host: View,
429                     info: AccessibilityNodeInfo
430                 ) {
431 
432                     super.onInitializeAccessibilityNodeInfo(host, info)
433                     info.addAction(AccessibilityAction(
434                         AccessibilityAction.ACTION_CLICK.id,
435                         context.getString(R.string.maximize_menu_talkback_action_maximize_restore_text)
436                     ))
437                     host.isClickable = true
438                 }
439 
performAccessibilityActionnull440                 override fun performAccessibilityAction(
441                     host: View,
442                     action: Int,
443                     args: Bundle?
444                 ): Boolean {
445                     if (action == AccessibilityAction.ACTION_CLICK.id) {
446                         desktopModeUiEventLogger.log(taskInfo, A11Y_MAXIMIZE_MENU_MAXIMIZE)
447                         onMaximizeClickListener?.invoke()
448                     }
449                     return super.performAccessibilityAction(host, action, args)
450                 }
451             }
452 
453             snapLeftButton.accessibilityDelegate = object : View.AccessibilityDelegate() {
onInitializeAccessibilityNodeInfonull454                 override fun onInitializeAccessibilityNodeInfo(
455                     host: View,
456                     info: AccessibilityNodeInfo
457                 ) {
458                     super.onInitializeAccessibilityNodeInfo(host, info)
459                     info.addAction(AccessibilityAction(
460                         AccessibilityAction.ACTION_CLICK.id,
461                         context.getString(R.string.maximize_menu_talkback_action_snap_left_text)
462                     ))
463                     host.isClickable = true
464                 }
465 
performAccessibilityActionnull466                 override fun performAccessibilityAction(
467                     host: View,
468                     action: Int,
469                     args: Bundle?
470                 ): Boolean {
471                     if (action == AccessibilityAction.ACTION_CLICK.id) {
472                         desktopModeUiEventLogger.log(taskInfo, A11Y_MAXIMIZE_MENU_RESIZE_LEFT)
473                         onLeftSnapClickListener?.invoke()
474                     }
475                     return super.performAccessibilityAction(host, action, args)
476                 }
477             }
478 
479             snapRightButton.accessibilityDelegate = object : View.AccessibilityDelegate() {
onInitializeAccessibilityNodeInfonull480                 override fun onInitializeAccessibilityNodeInfo(
481                     host: View,
482                     info: AccessibilityNodeInfo
483                 ) {
484                     super.onInitializeAccessibilityNodeInfo(host, info)
485                     info.addAction(AccessibilityAction(
486                         AccessibilityAction.ACTION_CLICK.id,
487                         context.getString(R.string.maximize_menu_talkback_action_snap_right_text)
488                     ))
489                     host.isClickable = true
490                 }
491 
performAccessibilityActionnull492                 override fun performAccessibilityAction(
493                     host: View,
494                     action: Int,
495                     args: Bundle?
496                 ): Boolean {
497                     if (action == AccessibilityAction.ACTION_CLICK.id) {
498                         desktopModeUiEventLogger.log(taskInfo, A11Y_MAXIMIZE_MENU_RESIZE_RIGHT)
499                         onRightSnapClickListener?.invoke()
500                     }
501                     return super.performAccessibilityAction(host, action, args)
502                 }
503             }
504 
505             // Maximize/restore button.
506             val sizeToggleBtnTextId = if (sizeToggleDirection == SizeToggleDirection.RESTORE)
507                 R.string.desktop_mode_maximize_menu_restore_button_text
508             else
509                 R.string.desktop_mode_maximize_menu_maximize_button_text
510             val sizeToggleBtnText = context.resources.getText(sizeToggleBtnTextId)
511             sizeToggleButton.contentDescription = sizeToggleBtnText
512             sizeToggleButtonText.text = sizeToggleBtnText
513 
514             // Immersive enter/exit button.
515             if (immersiveConfig is ImmersiveConfig.Visible) {
516                 val immersiveToggleBtnTextId = when (immersiveConfig.direction) {
517                     ImmersiveToggleDirection.ENTER -> {
518                         R.string.desktop_mode_maximize_menu_immersive_button_text
519                     }
520 
521                     ImmersiveToggleDirection.EXIT -> {
522                         R.string.desktop_mode_maximize_menu_immersive_restore_button_text
523                     }
524                 }
525                 val immersiveToggleBtnText = context.resources.getText(immersiveToggleBtnTextId)
526                 immersiveToggleButton.contentDescription = immersiveToggleBtnText
527                 immersiveToggleButtonText.text = immersiveToggleBtnText
528             }
529 
530             // To prevent aliasing.
531             sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
532             sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
533             immersiveToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
534             immersiveToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
535         }
536 
537         /** Bind the menu views to the new [RunningTaskInfo] data. */
bindnull538         fun bind(taskInfo: RunningTaskInfo) {
539             this.taskInfo = taskInfo
540             this.style = calculateMenuStyle(taskInfo)
541 
542             rootView.background.setTint(style.backgroundColor)
543 
544             // Maximize option.
545             sizeToggleButton.background = style.maximizeOption.drawable
546             sizeToggleButtonText.setTextColor(style.textColor)
547 
548             // Immersive option.
549             immersiveToggleButton.background = style.immersiveOption.drawable
550             immersiveToggleButtonText.setTextColor(style.textColor)
551 
552             // Snap options.
553             snapWindowText.setTextColor(style.textColor)
554             updateSplitSnapSelection(SnapToHalfSelection.NONE)
555         }
556 
557         /** Animate the opening of the menu */
animateOpenMenunull558         fun animateOpenMenu(onEnd: () -> Unit) {
559             sizeToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
560             sizeToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
561             immersiveToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
562             immersiveToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
563             menuAnimatorSet = AnimatorSet()
564             menuAnimatorSet?.playTogether(
565                 ObjectAnimator.ofFloat(rootView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f)
566                     .apply {
567                         duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS
568                         interpolator = EMPHASIZED_DECELERATE
569                     },
570                 ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f)
571                     .apply {
572                         duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS
573                         interpolator = EMPHASIZED_DECELERATE
574                         addUpdateListener {
575                             // Animate padding so that controls stay pinned to the bottom of
576                             // the menu.
577                             val value = animatedValue as Float
578                             val topPadding = menuPadding -
579                                     ((1 - value) * measureHeight()).toInt()
580                             container.setPadding(menuPadding, topPadding,
581                                 menuPadding, menuPadding)
582                         }
583                     },
584                 ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply {
585                     duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS
586                     interpolator = EMPHASIZED_DECELERATE
587                     addUpdateListener {
588                         // Scale up the children of the maximize menu so that the menu
589                         // scale is cancelled out and only the background is scaled.
590                         val value = animatedValue as Float
591                         sizeToggleButton.scaleY = value
592                         immersiveToggleButton.scaleY = value
593                         snapButtonsLayout.scaleY = value
594                         sizeToggleButtonText.scaleY = value
595                         immersiveToggleButtonText.scaleY = value
596                         snapWindowText.scaleY = value
597                     }
598                 },
599                 ObjectAnimator.ofFloat(rootView, TRANSLATION_Y,
600                     (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight(), 0f).apply {
601                     duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS
602                     interpolator = EMPHASIZED_DECELERATE
603                 },
604                 ObjectAnimator.ofInt(rootView.background, "alpha",
605                     MAX_DRAWABLE_ALPHA_VALUE).apply {
606                     duration = ALPHA_ANIMATION_DURATION_MS
607                 },
608                 ValueAnimator.ofFloat(0f, 1f)
609                     .apply {
610                         duration = ALPHA_ANIMATION_DURATION_MS
611                         startDelay = CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS
612                         addUpdateListener {
613                             val value = animatedValue as Float
614                             sizeToggleButton.alpha = value
615                             immersiveToggleButton.alpha = value
616                             snapButtonsLayout.alpha = value
617                             sizeToggleButtonText.alpha = value
618                             immersiveToggleButtonText.alpha = value
619                             snapWindowText.alpha = value
620                         }
621                     },
622                 ObjectAnimator.ofFloat(rootView, TRANSLATION_Z, MENU_Z_TRANSLATION)
623                     .apply {
624                         duration = ELEVATION_ANIMATION_DURATION_MS
625                         startDelay = CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS
626                     }
627             )
628             menuAnimatorSet?.addListener(
629                 onEnd = {
630                     sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
631                     sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
632                     immersiveToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
633                     immersiveToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
634                     onEnd.invoke()
635                 }
636             )
637             menuAnimatorSet?.start()
638         }
639 
640         /** Animate the closing of the menu */
animateCloseMenunull641         fun animateCloseMenu(onEnd: (() -> Unit)) {
642             sizeToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
643             sizeToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
644             immersiveToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
645             immersiveToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
646             cancelAnimation()
647             menuAnimatorSet = AnimatorSet()
648             menuAnimatorSet?.playTogether(
649                     ObjectAnimator.ofFloat(rootView, SCALE_Y, 1f, STARTING_MENU_HEIGHT_SCALE)
650                             .apply {
651                                 duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS
652                                 interpolator = FAST_OUT_LINEAR_IN
653                             },
654                     ValueAnimator.ofFloat(1f, STARTING_MENU_HEIGHT_SCALE)
655                             .apply {
656                                 duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS
657                                 interpolator = FAST_OUT_LINEAR_IN
658                                 addUpdateListener {
659                                     // Animate padding so that controls stay pinned to the bottom of
660                                     // the menu.
661                                     val value = animatedValue as Float
662                                     val topPadding = menuPadding -
663                                             ((1 - value) * measureHeight()).toInt()
664                                     container.setPadding(menuPadding, topPadding,
665                                             menuPadding, menuPadding)
666                                 }
667                             },
668                     ValueAnimator.ofFloat(1f, 1 / STARTING_MENU_HEIGHT_SCALE).apply {
669                         duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS
670                         interpolator = FAST_OUT_LINEAR_IN
671                         addUpdateListener {
672                             // Scale up the children of the maximize menu so that the menu
673                             // scale is cancelled out and only the background is scaled.
674                             val value = animatedValue as Float
675                             sizeToggleButton.scaleY = value
676                             immersiveToggleButton.scaleY = value
677                             snapButtonsLayout.scaleY = value
678                             sizeToggleButtonText.scaleY = value
679                             immersiveToggleButtonText.scaleY = value
680                             snapWindowText.scaleY = value
681                         }
682                     },
683                     ObjectAnimator.ofFloat(rootView, TRANSLATION_Y,
684                             0f, (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight()).apply {
685                         duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS
686                         interpolator = FAST_OUT_LINEAR_IN
687                     },
688                     ObjectAnimator.ofInt(rootView.background, "alpha",
689                             MAX_DRAWABLE_ALPHA_VALUE, 0).apply {
690                         startDelay = CONTAINER_ALPHA_CLOSE_MENU_ANIMATION_DELAY_MS
691                         duration = ALPHA_ANIMATION_DURATION_MS
692                     },
693                     ValueAnimator.ofFloat(1f, 0f)
694                             .apply {
695                                 duration = ALPHA_ANIMATION_DURATION_MS
696                                 addUpdateListener {
697                                     val value = animatedValue as Float
698                                     sizeToggleButton.alpha = value
699                                     immersiveToggleButton.alpha = value
700                                     snapButtonsLayout.alpha = value
701                                     sizeToggleButtonText.alpha = value
702                                     immersiveToggleButtonText.alpha = value
703                                     snapWindowText.alpha = value
704                                 }
705                             },
706                     ObjectAnimator.ofFloat(rootView, TRANSLATION_Z, MENU_Z_TRANSLATION, 0f)
707                             .apply {
708                                 duration = ELEVATION_ANIMATION_DURATION_MS
709                             }
710             )
711             menuAnimatorSet?.addListener(
712                     onEnd = {
713                         sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
714                         sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
715                         immersiveToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
716                         immersiveToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
717                         onEnd?.invoke()
718                     }
719             )
720             menuAnimatorSet?.start()
721         }
722 
723         /** Request that the accessibility service focus on the menu. */
requestAccessibilityFocusnull724         fun requestAccessibilityFocus() {
725             // Focus the first button in the menu by default.
726             if (immersiveToggleButton.isVisible) {
727                 immersiveToggleButton.post {
728                     immersiveToggleButton.sendAccessibilityEvent(
729                         AccessibilityEvent.TYPE_VIEW_FOCUSED
730                     )
731                 }
732                 return
733             }
734             sizeToggleButton.post {
735                 sizeToggleButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
736             }
737         }
738 
739         /** Cancel the menu animation. */
cancelAnimationnull740         private fun cancelAnimation() {
741             menuAnimatorSet?.cancel()
742         }
743 
744         /** Update the view state to a new snap to half selection. */
updateSplitSnapSelectionnull745         private fun updateSplitSnapSelection(selection: SnapToHalfSelection) {
746             when (selection) {
747                 SnapToHalfSelection.NONE -> deactivateSnapOptions()
748                 SnapToHalfSelection.LEFT -> activateSnapOption(activateLeft = true)
749                 SnapToHalfSelection.RIGHT -> activateSnapOption(activateLeft = false)
750             }
751         }
752 
calculateMenuStylenull753         private fun calculateMenuStyle(taskInfo: RunningTaskInfo): MenuStyle {
754             val colorScheme = decorThemeUtil.getColorScheme(taskInfo)
755             val menuBackgroundColor = colorScheme.surfaceContainerLow.toArgb()
756             return MenuStyle(
757                 backgroundColor = menuBackgroundColor,
758                 textColor = colorScheme.onSurface.toArgb(),
759                 maximizeOption = MenuStyle.MaximizeOption(
760                     drawable = createMaximizeOrImmersiveDrawable(
761                         menuBackgroundColor,
762                         colorScheme,
763                         fillPadding = when (sizeToggleDirection) {
764                             SizeToggleDirection.MAXIMIZE -> maximizeFillPaddingRect
765                             SizeToggleDirection.RESTORE -> maximizeRestoreFillPaddingRect
766                         }
767                     )
768                 ),
769                 immersiveOption = MenuStyle.ImmersiveOption(
770                     drawable = createMaximizeOrImmersiveDrawable(
771                         menuBackgroundColor,
772                         colorScheme,
773                         fillPadding = immersiveFillPaddingRect,
774                     ),
775                 ),
776                 snapOptions = MenuStyle.SnapOptions(
777                     inactiveSnapSideColor = colorScheme.outlineVariant.toArgb(),
778                     semiActiveSnapSideColor = colorScheme.primary.toArgb().withAlpha(OPACITY_40),
779                     activeSnapSideColor = colorScheme.primary.toArgb(),
780                     inactiveStrokeColor = colorScheme.outlineVariant.toArgb().withAlpha(OPACITY_60),
781                     activeStrokeColor = colorScheme.primary.toArgb(),
782                     inactiveBackgroundColor = menuBackgroundColor,
783                     activeBackgroundColor = colorScheme.primary.toArgb().withAlpha(OPACITY_12)
784                 ),
785             )
786         }
787 
788         /** Measure width of the root view of this menu. */
measureWidthnull789         fun measureWidth(): Int {
790             rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
791             return rootView.measuredWidth
792         }
793 
794         /** Measure height of the root view of this menu. */
measureHeightnull795         fun measureHeight(): Int {
796             rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
797             return rootView.measuredHeight
798         }
799 
deactivateSnapOptionsnull800         private fun deactivateSnapOptions() {
801             // TODO(b/346440693): the background/colorStateList set on these buttons is overridden
802             //  to a static resource & color on manually tracked hover events, which defeats the
803             //  point of state lists and selector states. Look into whether changing that is
804             //  possible, similar to the maximize option. Also to include support for the
805             //  semi-active state (when the "other" snap option is selected).
806             val snapSideColorList = ColorStateList(
807                 arrayOf(
808                     intArrayOf(android.R.attr.state_pressed),
809                     intArrayOf(android.R.attr.state_focused),
810                     intArrayOf(android.R.attr.state_selected),
811                     intArrayOf(),
812                 ),
813                 intArrayOf(
814                     style.snapOptions.activeSnapSideColor,
815                     style.snapOptions.activeSnapSideColor,
816                     style.snapOptions.activeSnapSideColor,
817                     style.snapOptions.inactiveSnapSideColor
818                 )
819             )
820             snapLeftButton.background?.setTintList(snapSideColorList)
821             snapRightButton.background?.setTintList(snapSideColorList)
822             with (snapButtonsLayout) {
823                 setBackgroundResource(R.drawable.desktop_mode_maximize_menu_layout_background)
824                 (background as GradientDrawable).apply {
825                     setColor(style.snapOptions.inactiveBackgroundColor)
826                     setStroke(outlineStroke, style.snapOptions.inactiveStrokeColor)
827                 }
828             }
829         }
830 
activateSnapOptionnull831         private fun activateSnapOption(activateLeft: Boolean) {
832             // Regardless of which side is active, the background of the snap options layout (that
833             // includes both sides) is considered "active".
834             with (snapButtonsLayout) {
835                 setBackgroundResource(
836                     R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
837                 (background as GradientDrawable).apply {
838                     setColor(style.snapOptions.activeBackgroundColor)
839                     setStroke(outlineStroke, style.snapOptions.activeStrokeColor)
840                 }
841             }
842             if (activateLeft) {
843                 // Highlight snap left button, partially highlight the other side.
844                 snapLeftButton.background.setTint(style.snapOptions.activeSnapSideColor)
845                 snapRightButton.background.setTint(style.snapOptions.semiActiveSnapSideColor)
846             } else {
847                 // Highlight snap right button, partially highlight the other side.
848                 snapRightButton.background.setTint(style.snapOptions.activeSnapSideColor)
849                 snapLeftButton.background.setTint(style.snapOptions.semiActiveSnapSideColor)
850             }
851         }
852 
createMaximizeOrImmersiveDrawablenull853         private fun createMaximizeOrImmersiveDrawable(
854             @ColorInt menuBackgroundColor: Int,
855             colorScheme: ColorScheme,
856             fillPadding: Rect,
857         ): StateListDrawable {
858             val activeStrokeAndFill = colorScheme.primary.toArgb()
859             val activeBackground = colorScheme.primary.toArgb().withAlpha(OPACITY_12)
860             val activeDrawable = createMaximizeOrImmersiveButtonDrawable(
861                 strokeColor = activeStrokeAndFill,
862                 fillColor = activeStrokeAndFill,
863                 backgroundColor = activeBackground,
864                 // Add a mask with the menu background's color because the active background color is
865                 // semi transparent, otherwise the transparency will reveal the stroke/fill color
866                 // behind it.
867                 backgroundMask = menuBackgroundColor,
868                 fillPadding = fillPadding,
869             )
870             return StateListDrawable().apply {
871                 addState(intArrayOf(android.R.attr.state_pressed), activeDrawable)
872                 addState(intArrayOf(android.R.attr.state_focused), activeDrawable)
873                 addState(intArrayOf(android.R.attr.state_selected), activeDrawable)
874                 addState(intArrayOf(android.R.attr.state_hovered), activeDrawable)
875                 // Inactive drawable.
876                 addState(
877                     StateSet.WILD_CARD,
878                     createMaximizeOrImmersiveButtonDrawable(
879                         strokeColor = colorScheme.outlineVariant.toArgb().withAlpha(OPACITY_60),
880                         fillColor = colorScheme.outlineVariant.toArgb(),
881                         backgroundColor = colorScheme.surfaceContainerLow.toArgb(),
882                         backgroundMask = null, // not needed because the bg color is fully opaque
883                         fillPadding = fillPadding,
884                     )
885                 )
886             }
887         }
888 
createMaximizeOrImmersiveButtonDrawablenull889         private fun createMaximizeOrImmersiveButtonDrawable(
890             @ColorInt strokeColor: Int,
891             @ColorInt fillColor: Int,
892             @ColorInt backgroundColor: Int,
893             @ColorInt backgroundMask: Int?,
894             fillPadding: Rect,
895         ): LayerDrawable {
896             val layers = mutableListOf<Drawable>()
897             // First (bottom) layer, effectively the button's border ring once its inner shape is
898             // covered by the next layers.
899             layers.add(ShapeDrawable().apply {
900                 shape = RoundRectShape(
901                     FloatArray(8) { outlineRadius.toFloat() },
902                     null /* inset */,
903                     null /* innerRadii */
904                 )
905                 paint.color = strokeColor
906                 paint.style = Paint.Style.FILL
907             })
908             // Second layer, a mask for the next (background) layer if needed because of
909             // transparency.
910             backgroundMask?.let { color ->
911                 layers.add(
912                     ShapeDrawable().apply {
913                         shape = RoundRectShape(
914                             FloatArray(8) { outlineRadius.toFloat() },
915                             null /* inset */,
916                             null /* innerRadii */
917                         )
918                         paint.color = color
919                         paint.style = Paint.Style.FILL
920                     }
921                 )
922             }
923             // Third layer, the "background" padding between the border and the fill.
924             layers.add(ShapeDrawable().apply {
925                 shape = RoundRectShape(
926                     FloatArray(8) { outlineRadius.toFloat() },
927                     null /* inset */,
928                     null /* innerRadii */
929                 )
930                 paint.color = backgroundColor
931                 paint.style = Paint.Style.FILL
932             })
933             // Final layer, the inner most rounded-rect "fill".
934             layers.add(ShapeDrawable().apply {
935                 shape = RoundRectShape(
936                     FloatArray(8) { fillRadius.toFloat() },
937                     null /* inset */,
938                     null /* innerRadii */
939                 )
940                 paint.color = fillColor
941                 paint.style = Paint.Style.FILL
942             })
943 
944             return LayerDrawable(layers.toTypedArray()).apply {
945                 when (numberOfLayers) {
946                     3 -> {
947                         setLayerInset(1, outlineStroke)
948                         setLayerInset(2, fillPadding.left, fillPadding.top,
949                             fillPadding.right, fillPadding.bottom)
950                     }
951                     4 -> {
952                         setLayerInset(intArrayOf(1, 2), outlineStroke)
953                         setLayerInset(3, fillPadding.left, fillPadding.top,
954                             fillPadding.right, fillPadding.bottom)
955                     }
956                     else -> error("Unexpected number of layers: $numberOfLayers")
957                 }
958             }
959         }
960 
LayerDrawablenull961         private fun LayerDrawable.setLayerInset(index: IntArray, inset: Int) {
962             for (i in index) {
963                 setLayerInset(i, inset, inset, inset, inset)
964             }
965         }
966 
LayerDrawablenull967         private fun LayerDrawable.setLayerInset(index: Int, inset: Int) {
968             setLayerInset(index, inset, inset, inset, inset)
969         }
970 
requireViewByIdnull971         private fun requireViewById(id: Int) = rootView.requireViewById<View>(id)
972 
973         /** The style to apply to the menu. */
974         data class MenuStyle(
975             @ColorInt val backgroundColor: Int,
976             @ColorInt val textColor: Int,
977             val maximizeOption: MaximizeOption,
978             val immersiveOption: ImmersiveOption,
979             val snapOptions: SnapOptions,
980         ) {
981             data class MaximizeOption(
982                 val drawable: StateListDrawable,
983             )
984             data class ImmersiveOption(
985                 val drawable: StateListDrawable,
986             )
987             data class SnapOptions(
988                 @ColorInt val inactiveSnapSideColor: Int,
989                 @ColorInt val semiActiveSnapSideColor: Int,
990                 @ColorInt val activeSnapSideColor: Int,
991                 @ColorInt val inactiveStrokeColor: Int,
992                 @ColorInt val activeStrokeColor: Int,
993                 @ColorInt val inactiveBackgroundColor: Int,
994                 @ColorInt val activeBackgroundColor: Int,
995             )
996         }
997 
998         /** The possible selection states of the half-snap menu option. */
999         enum class SnapToHalfSelection {
1000             NONE, LEFT, RIGHT
1001         }
1002 
1003         /** The possible immersive configs for this menu instance. */
1004         sealed class ImmersiveConfig {
1005             data class Visible(
1006                 val direction: ImmersiveToggleDirection,
1007             ) : ImmersiveConfig()
1008             data object Hidden : ImmersiveConfig()
1009         }
1010 
1011         /** The possible selection states of the size toggle button in the maximize menu. */
1012         enum class SizeToggleDirection {
1013             MAXIMIZE, RESTORE
1014         }
1015 
1016         /** The possible selection states of the immersive toggle button in the maximize menu. */
1017         enum class ImmersiveToggleDirection {
1018             ENTER, EXIT
1019         }
1020     }
1021 
1022     companion object {
1023         // Open menu animation constants
1024         private const val ALPHA_ANIMATION_DURATION_MS = 50L
1025         private const val MAX_DRAWABLE_ALPHA_VALUE = 255
1026         private const val STARTING_MENU_HEIGHT_SCALE = 0.8f
1027         private const val OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS = 300L
1028         private const val CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS = 200L
1029         private const val ELEVATION_ANIMATION_DURATION_MS = 50L
1030         private const val CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS = 33L
1031         private const val CONTAINER_ALPHA_CLOSE_MENU_ANIMATION_DELAY_MS = 33L
1032         private const val MENU_Z_TRANSLATION = 1f
1033     }
1034 }
1035 
1036 /** A factory interface to create a [MaximizeMenu]. */
1037 interface MaximizeMenuFactory {
createnull1038     fun create(
1039         syncQueue: SyncTransactionQueue,
1040         rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
1041         displayController: DisplayController,
1042         taskInfo: RunningTaskInfo,
1043         decorWindowContext: Context,
1044         positionSupplier: (Int, Int) -> Point,
1045         transactionSupplier: Supplier<Transaction>,
1046         desktopModeUiEventLogger: DesktopModeUiEventLogger,
1047     ): MaximizeMenu
1048 }
1049 
1050 /** A [MaximizeMenuFactory] implementation that creates a [MaximizeMenu].  */
1051 object DefaultMaximizeMenuFactory : MaximizeMenuFactory {
1052     override fun create(
1053         syncQueue: SyncTransactionQueue,
1054         rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
1055         displayController: DisplayController,
1056         taskInfo: RunningTaskInfo,
1057         decorWindowContext: Context,
1058         positionSupplier: (Int, Int) -> Point,
1059         transactionSupplier: Supplier<Transaction>,
1060         desktopModeUiEventLogger: DesktopModeUiEventLogger,
1061     ): MaximizeMenu {
1062         return MaximizeMenu(
1063             syncQueue,
1064             rootTdaOrganizer,
1065             displayController,
1066             taskInfo,
1067             decorWindowContext,
1068             positionSupplier,
1069             transactionSupplier,
1070             desktopModeUiEventLogger
1071         )
1072     }
1073 }
1074