• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.app.ActivityManager.RunningTaskInfo
19 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
20 import android.content.res.ColorStateList
21 import android.graphics.Color
22 import android.graphics.Point
23 import android.hardware.input.InputManager
24 import android.os.Bundle
25 import android.os.Handler
26 import android.view.MotionEvent.ACTION_DOWN
27 import android.view.SurfaceControl
28 import android.view.View
29 import android.view.View.OnClickListener
30 import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
31 import android.view.WindowManager
32 import android.view.accessibility.AccessibilityEvent
33 import android.view.accessibility.AccessibilityNodeInfo
34 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
35 import android.widget.ImageButton
36 import android.window.DesktopModeFlags
37 import androidx.core.view.ViewCompat
38 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
39 import com.android.internal.policy.SystemBarUtils
40 import com.android.window.flags.Flags
41 import com.android.wm.shell.R
42 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger
43 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_HANDLE_MENU_OPENED
44 import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
45 import com.android.wm.shell.windowdecor.AppHandleAnimator
46 import com.android.wm.shell.windowdecor.WindowManagerWrapper
47 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
48 
49 /**
50  * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split).
51  * It hosts a simple handle bar from which to initiate a drag motion to enter desktop mode.
52  */
53 class AppHandleViewHolder(
54     rootView: View,
55     onCaptionTouchListener: View.OnTouchListener,
56     onCaptionButtonClickListener: OnClickListener,
57     private val windowManagerWrapper: WindowManagerWrapper,
58     private val handler: Handler,
59     private val desktopModeUiEventLogger: DesktopModeUiEventLogger,
60 ) : WindowDecorationViewHolder<AppHandleViewHolder.HandleData>(rootView) {
61 
62     data class HandleData(
63         val taskInfo: RunningTaskInfo,
64         val position: Point,
65         val width: Int,
66         val height: Int,
67         val showInputLayer: Boolean,
68         val isCaptionVisible: Boolean,
69     ) : Data()
70 
71     private lateinit var taskInfo: RunningTaskInfo
72     private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
73     private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
74     private val inputManager = context.getSystemService(InputManager::class.java)
75     private val animator: AppHandleAnimator = AppHandleAnimator(rootView, captionHandle)
76     private var statusBarInputLayerExists = false
77 
78     // An invisible View that takes up the same coordinates as captionHandle but is layered
79     // above the status bar. The purpose of this View is to receive input intended for
80     // captionHandle.
81     private var statusBarInputLayer: AdditionalSystemViewContainer? = null
82 
83     init {
84         captionView.setOnTouchListener(onCaptionTouchListener)
85         captionHandle.setOnTouchListener(onCaptionTouchListener)
86         captionHandle.setOnClickListener(onCaptionButtonClickListener)
87         captionHandle.accessibilityDelegate = object : View.AccessibilityDelegate() {
88             override fun sendAccessibilityEvent(host: View, eventType: Int) {
89                 when (eventType) {
90                     AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
91                     AccessibilityEvent.TYPE_VIEW_HOVER_EXIT -> {
92                         // Caption Handle itself can't get a11y focus because it's under the status
93                         // bar, so pass through TYPE_VIEW_HOVER a11y events to the status bar
94                         // input layer, so that it can get a11y focus on the caption handle's behalf
95                         statusBarInputLayer?.view?.sendAccessibilityEvent(eventType)
96                     }
97                     else -> super.sendAccessibilityEvent(host, eventType)
98                 }
99             }
100         }
101     }
102 
103     override fun bindData(data: HandleData) {
104         bindData(
105             data.taskInfo,
106             data.position,
107             data.width,
108             data.height,
109             data.showInputLayer,
110             data.isCaptionVisible
111         )
112     }
113 
114     private fun bindData(
115         taskInfo: RunningTaskInfo,
116         position: Point,
117         width: Int,
118         height: Int,
119         showInputLayer: Boolean,
120         isCaptionVisible: Boolean
121     ) {
122         setVisibility(isCaptionVisible)
123         captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
124         this.taskInfo = taskInfo
125         // If handle is not in status bar region(i.e., bottom stage in vertical split),
126         // do not create an input layer
127         if (position.y >= SystemBarUtils.getStatusBarHeight(context) || !showInputLayer) {
128             disposeStatusBarInputLayer()
129             return
130         }
131         // Input layer view creation / modification takes a significant amount of time;
132         // post them so we don't hold up DesktopModeWindowDecoration#relayout.
133         if (statusBarInputLayerExists) {
134             handler.post { updateStatusBarInputLayer(position) }
135         } else {
136             // Input layer is created on a delay; prevent multiple from being created.
137             statusBarInputLayerExists = true
138             handler.post { createStatusBarInputLayer(position, width, height) }
139         }
140     }
141 
142     override fun onHandleMenuOpened() {
143         animator.animateCaptionHandleAlpha(startValue = 1f, endValue = 0f)
144     }
145 
146     override fun onHandleMenuClosed() {
147         animator.animateCaptionHandleAlpha(startValue = 0f, endValue = 1f)
148     }
149 
150     private fun createStatusBarInputLayer(handlePosition: Point,
151                                           handleWidth: Int,
152                                           handleHeight: Int) {
153         if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return
154         statusBarInputLayer = AdditionalSystemViewContainer(context, windowManagerWrapper,
155             taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight,
156             WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
157             ignoreCutouts = Flags.showAppHandleLargeScreens()
158                     || BubbleAnythingFlagHelper.enableBubbleToFullscreen()
159         )
160         val view = statusBarInputLayer?.view ?: error("Unable to find statusBarInputLayer View")
161         val lp = statusBarInputLayer?.lp ?: error("Unable to find statusBarInputLayer " +
162                 "LayoutParams")
163         lp.title = "Handle Input Layer of task " + taskInfo.taskId
164         lp.setTrustedOverlay()
165         // Make this window a spy window to enable it to pilfer pointers from the system-wide
166         // gesture listener that receives events before window. This is to prevent notification
167         // shade gesture when we swipe down to enter desktop.
168         lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
169         view.setOnHoverListener { _, event ->
170             captionHandle.onHoverEvent(event)
171         }
172         // Caption handle is located within the status bar region, meaning the
173         // DisplayPolicy will attempt to transfer this input to status bar if it's
174         // a swipe down. Pilfer here to keep the gesture in handle alone.
175         view.setOnTouchListener { v, event ->
176             if (event.actionMasked == ACTION_DOWN) {
177                 inputManager.pilferPointers(v.viewRootImpl.inputToken)
178             }
179             captionHandle.dispatchTouchEvent(event)
180             return@setOnTouchListener true
181         }
182         setupAppHandleA11y(view)
183         windowManagerWrapper.updateViewLayout(view, lp)
184     }
185 
186     private fun setupAppHandleA11y(view: View) {
187         view.accessibilityDelegate = object : View.AccessibilityDelegate() {
188             override fun onInitializeAccessibilityNodeInfo(
189                 host: View,
190                 info: AccessibilityNodeInfo
191             ) {
192                 // Allow the status bar input layer to be a11y clickable so it can interact with
193                 // a11y services on behalf of caption handle (due to being under status bar)
194                 super.onInitializeAccessibilityNodeInfo(host, info)
195                 info.addAction(AccessibilityAction.ACTION_CLICK)
196                 host.isClickable = true
197             }
198 
199             override fun performAccessibilityAction(
200                 host: View,
201                 action: Int,
202                 args: Bundle?
203             ): Boolean {
204                 // Passthrough the a11y click action so the caption handle, so that app handle menu
205                 // is opened on a11y click, similar to a real click
206                 if (action == AccessibilityAction.ACTION_CLICK.id) {
207                     desktopModeUiEventLogger.log(taskInfo, A11Y_APP_HANDLE_MENU_OPENED)
208                     captionHandle.performClick()
209                 }
210                 return super.performAccessibilityAction(host, action, args)
211             }
212 
213             override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {
214                 super.onPopulateAccessibilityEvent(host, event)
215                 // When the status bar input layer is focused, use the content description of the
216                 // caption handle so that it appears as "App handle" and not "Unlabelled view"
217                 if (event.eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
218                     event.text.add(captionHandle.contentDescription)
219                 }
220             }
221         }
222 
223         // Update a11y action text so that Talkback announces "Press double tap to open menu"
224         // while focused on status bar input layer
225         ViewCompat.replaceAccessibilityAction(
226             view,
227             AccessibilityActionCompat.ACTION_CLICK,
228             context.getString(R.string.app_handle_chip_accessibility_announce),
229             null
230         )
231     }
232 
233     private fun updateStatusBarInputLayer(globalPosition: Point) {
234         statusBarInputLayer?.setPosition(
235             SurfaceControl.Transaction(),
236             globalPosition.x.toFloat(),
237             globalPosition.y.toFloat()
238         ) ?: return
239     }
240 
241     /**
242      * Remove the input layer from [WindowManager]. Should be used when caption handle
243      * is not visible.
244      */
245     fun disposeStatusBarInputLayer() {
246         if (!statusBarInputLayerExists) return
247         statusBarInputLayerExists = false
248         statusBarInputLayer?.view?.setOnTouchListener(null)
249         handler.post {
250             statusBarInputLayer?.releaseView()
251             statusBarInputLayer = null
252         }
253     }
254 
255     private fun setVisibility(visible: Boolean) {
256         val v = if (visible) View.VISIBLE else View.GONE
257         if (
258             captionView.visibility == v ||
259                 !DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()
260         ) {
261             return
262         }
263         animator.animateVisibilityChange(v)
264     }
265 
266     private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
267         return if (shouldUseLightCaptionColors(taskInfo)) {
268             context.getColor(R.color.desktop_mode_caption_handle_bar_light)
269         } else {
270             context.getColor(R.color.desktop_mode_caption_handle_bar_dark)
271         }
272     }
273 
274     /**
275      * Whether the caption items should use the 'light' color variant so that there's good contrast
276      * with the caption background color.
277      */
278     private fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean {
279         return taskInfo.taskDescription
280             ?.let { taskDescription ->
281                 if (Color.alpha(taskDescription.statusBarColor) != 0 &&
282                     taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
283                 ) {
284                     Color.valueOf(taskDescription.statusBarColor).luminance() < 0.5
285                 } else {
286                     taskDescription.systemBarsAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0
287                 }
288             } ?: false
289     }
290 
291     override fun close() {
292         animator.cancel()
293     }
294 
295     /** Factory class for creating [AppHandleViewHolder] objects. */
296     class Factory {
297         /**
298          * Create a [AppHandleViewHolder] object to handle caption view and status bar
299          * input layer logic.
300          */
301         fun create(
302             rootView: View,
303             onCaptionTouchListener: View.OnTouchListener,
304             onCaptionButtonClickListener: OnClickListener,
305             windowManagerWrapper: WindowManagerWrapper,
306             handler: Handler,
307             desktopModeUiEventLogger: DesktopModeUiEventLogger,
308         ): AppHandleViewHolder = AppHandleViewHolder(
309             rootView,
310             onCaptionTouchListener,
311             onCaptionButtonClickListener,
312             windowManagerWrapper,
313             handler,
314             desktopModeUiEventLogger,
315         )
316     }
317 }
318