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