• 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 
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