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