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 17 package com.android.systemui.screenshot.ui 18 19 import android.content.Context 20 import android.content.res.Configuration 21 import android.graphics.Insets 22 import android.graphics.Rect 23 import android.graphics.Region 24 import android.util.AttributeSet 25 import android.view.GestureDetector 26 import android.view.GestureDetector.SimpleOnGestureListener 27 import android.view.MotionEvent 28 import android.view.View 29 import android.view.ViewGroup 30 import android.view.WindowInsets 31 import android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL 32 import android.view.accessibility.AccessibilityEvent 33 import android.widget.FrameLayout 34 import android.widget.ImageView 35 import com.android.systemui.res.R 36 import com.android.systemui.screenshot.FloatingWindowUtil 37 38 class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) : 39 FrameLayout(context, attrs) { 40 lateinit var screenshotPreview: ImageView 41 lateinit var blurredScreenshotPreview: ImageView 42 private lateinit var screenshotStatic: ViewGroup 43 var onTouchInterceptListener: ((MotionEvent) -> Boolean)? = null 44 45 var userInteractionCallback: (() -> Unit)? = null 46 47 private val displayMetrics = context.resources.displayMetrics 48 private val tmpRect = Rect() 49 private lateinit var actionsContainerBackground: View 50 private lateinit var actionsContainer: View 51 private lateinit var dismissButton: View 52 53 // Prepare an internal `GestureDetector` to determine when we can initiate a touch-interception 54 // session (with the client's provided `onTouchInterceptListener`). We delegate out to their 55 // listener only for gestures that can't be handled by scrolling our `actionsContainer`. 56 private val gestureDetector = 57 GestureDetector( 58 context, 59 object : SimpleOnGestureListener() { 60 override fun onScroll( 61 ev1: MotionEvent?, 62 ev2: MotionEvent, 63 distanceX: Float, 64 distanceY: Float, 65 ): Boolean { 66 actionsContainer.getBoundsOnScreen(tmpRect) 67 val touchedInActionsContainer = 68 tmpRect.contains(ev2.rawX.toInt(), ev2.rawY.toInt()) 69 val canHandleInternallyByScrolling = 70 touchedInActionsContainer && 71 actionsContainer.canScrollHorizontally(distanceX.toInt()) 72 return !canHandleInternallyByScrolling 73 } 74 }, 75 ) 76 77 init { 78 79 // Delegate to the client-provided `onTouchInterceptListener` if we've already initiated 80 // touch-interception. 81 setOnTouchListener({ _: View, ev: MotionEvent -> 82 userInteractionCallback?.invoke() 83 onTouchInterceptListener?.invoke(ev) ?: false 84 }) 85 86 gestureDetector.setIsLongpressEnabled(false) 87 88 // Extend the timeout on any accessibility event (e.g. voice access or explore-by-touch). 89 setAccessibilityDelegate( 90 object : AccessibilityDelegate() { 91 override fun onRequestSendAccessibilityEvent( 92 host: ViewGroup, 93 child: View, 94 event: AccessibilityEvent, 95 ): Boolean { 96 userInteractionCallback?.invoke() 97 return super.onRequestSendAccessibilityEvent(host, child, event) 98 } 99 } 100 ) 101 } 102 103 override fun onFinishInflate() { 104 super.onFinishInflate() 105 // Get focus so that the key events go to the layout. 106 isFocusableInTouchMode = true 107 screenshotPreview = requireViewById(R.id.screenshot_preview) 108 blurredScreenshotPreview = requireViewById(R.id.screenshot_preview_blur) 109 screenshotStatic = requireViewById(R.id.screenshot_static) 110 actionsContainerBackground = requireViewById(R.id.actions_container_background) 111 actionsContainer = requireViewById(R.id.actions_container) 112 dismissButton = requireViewById(R.id.screenshot_dismiss_button) 113 114 // Configure to extend the timeout during ongoing gestures (i.e. scrolls) that are already 115 // being handled by our child views. 116 actionsContainer.setOnTouchListener({ _: View, ev: MotionEvent -> 117 userInteractionCallback?.invoke() 118 false 119 }) 120 } 121 122 fun getTouchRegion(gestureInsets: Insets): Region { 123 val region = getSwipeRegion() 124 125 // only add gesture insets to touch region in gestural mode 126 if ( 127 resources.getInteger(com.android.internal.R.integer.config_navBarInteractionMode) == 128 NAV_BAR_MODE_GESTURAL 129 ) { 130 // Receive touches in gesture insets so they don't cause TOUCH_OUTSIDE 131 // left edge gesture region 132 val insetRect = Rect(0, 0, gestureInsets.left, displayMetrics.heightPixels) 133 region.op(insetRect, Region.Op.UNION) 134 // right edge gesture region 135 insetRect.set( 136 displayMetrics.widthPixels - gestureInsets.right, 137 0, 138 displayMetrics.widthPixels, 139 displayMetrics.heightPixels, 140 ) 141 region.op(insetRect, Region.Op.UNION) 142 } 143 144 return region 145 } 146 147 fun updateInsets(insets: WindowInsets) { 148 val orientation = mContext.resources.configuration.orientation 149 val inPortrait = orientation == Configuration.ORIENTATION_PORTRAIT 150 val cutout = insets.displayCutout 151 val navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()) 152 153 // When honoring the navbar or other obstacle offsets, include some extra padding above 154 // the inset itself. 155 val verticalPadding = 156 mContext.resources.getDimensionPixelOffset(R.dimen.screenshot_shelf_vertical_margin) 157 158 // Minimum bottom padding to always enforce (e.g. if there's no nav bar) 159 val minimumBottomPadding = 160 context.resources.getDimensionPixelOffset( 161 R.dimen.overlay_action_container_minimum_edge_spacing 162 ) 163 164 if (cutout == null) { 165 screenshotStatic.setPadding( 166 navBarInsets.left, 167 navBarInsets.top, 168 navBarInsets.right, 169 navBarInsets.bottom, 170 ) 171 } else { 172 val waterfall = cutout.waterfallInsets 173 if (inPortrait) { 174 screenshotStatic.setPadding( 175 waterfall.left, 176 max(cutout.safeInsetTop, waterfall.top), 177 waterfall.right, 178 max( 179 navBarInsets.bottom + verticalPadding, 180 cutout.safeInsetBottom + verticalPadding, 181 waterfall.bottom + verticalPadding, 182 minimumBottomPadding, 183 ), 184 ) 185 } else { 186 screenshotStatic.setPadding( 187 max(cutout.safeInsetLeft, waterfall.left, navBarInsets.left), 188 waterfall.top, 189 max(cutout.safeInsetRight, waterfall.right, navBarInsets.right), 190 max( 191 navBarInsets.bottom + verticalPadding, 192 waterfall.bottom + verticalPadding, 193 minimumBottomPadding, 194 ), 195 ) 196 } 197 } 198 } 199 200 // Max function for two or more params. 201 private fun max(first: Int, second: Int, vararg items: Int): Int { 202 var largest = if (first > second) first else second 203 for (item in items) { 204 if (item > largest) { 205 largest = item 206 } 207 } 208 return largest 209 } 210 211 private fun getSwipeRegion(): Region { 212 val swipeRegion = Region() 213 val padding = FloatingWindowUtil.dpToPx(displayMetrics, -1 * TOUCH_PADDING_DP).toInt() 214 swipeRegion.addInsetView(screenshotPreview, padding) 215 swipeRegion.addInsetView(actionsContainerBackground, padding) 216 swipeRegion.addInsetView(dismissButton, padding) 217 findViewById<View>(R.id.screenshot_message_container)?.let { 218 swipeRegion.addInsetView(it, padding) 219 } 220 return swipeRegion 221 } 222 223 private fun Region.addInsetView(view: View, padding: Int = 0) { 224 view.getBoundsOnScreen(tmpRect) 225 tmpRect.inset(padding, padding) 226 this.op(tmpRect, Region.Op.UNION) 227 } 228 229 companion object { 230 private const val TOUCH_PADDING_DP = 12f 231 } 232 233 override fun onInterceptHoverEvent(event: MotionEvent): Boolean { 234 userInteractionCallback?.invoke() 235 return super.onInterceptHoverEvent(event) 236 } 237 238 override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { 239 userInteractionCallback?.invoke() 240 241 // Let the client-provided listener see all `DOWN` events so that they'll be able to 242 // interpret the remainder of the gesture, even if interception starts partway-through. 243 // TODO: is this really necessary? And if we don't go on to start interception, should we 244 // follow up with `ACTION_CANCEL`? 245 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 246 onTouchInterceptListener?.invoke(ev) 247 } 248 249 // Only allow the client-provided touch interceptor to take over the gesture if our 250 // top-level `GestureDetector` decides not to scroll the action container. 251 return gestureDetector.onTouchEvent(ev) 252 } 253 } 254