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
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.content.Context
22 import android.graphics.Bitmap
23 import android.graphics.Rect
24 import android.graphics.Region
25 import android.os.Looper
26 import android.view.Choreographer
27 import android.view.InputEvent
28 import android.view.KeyEvent
29 import android.view.LayoutInflater
30 import android.view.MotionEvent
31 import android.view.ScrollCaptureResponse
32 import android.view.View
33 import android.view.ViewTreeObserver
34 import android.view.WindowInsets
35 import android.view.WindowManager
36 import android.window.OnBackInvokedCallback
37 import android.window.OnBackInvokedDispatcher
38 import androidx.appcompat.content.res.AppCompatResources
39 import androidx.core.animation.doOnEnd
40 import androidx.core.animation.doOnStart
41 import com.android.internal.logging.UiEventLogger
42 import com.android.systemui.log.DebugLogger.debugLog
43 import com.android.systemui.res.R
44 import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
45 import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT
46 import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW
47 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
48 import com.android.systemui.screenshot.scroll.ScrollCaptureController
49 import com.android.systemui.screenshot.ui.ScreenshotAnimationController
50 import com.android.systemui.screenshot.ui.ScreenshotShelfView
51 import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder
52 import com.android.systemui.screenshot.ui.viewmodel.AnimationState
53 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
54 import com.android.systemui.shared.system.InputChannelCompat
55 import com.android.systemui.shared.system.InputMonitorCompat
56 import dagger.assisted.Assisted
57 import dagger.assisted.AssistedFactory
58 import dagger.assisted.AssistedInject
59 
60 /** Controls the screenshot view and viewModel. */
61 class ScreenshotShelfViewProxy
62 @AssistedInject
63 constructor(
64     private val logger: UiEventLogger,
65     private val viewModel: ScreenshotViewModel,
66     private val windowManager: WindowManager,
67     shelfViewBinder: ScreenshotShelfViewBinder,
68     private val thumbnailObserver: ThumbnailObserver,
69     @Assisted private val context: Context,
70     @Assisted private val displayId: Int,
71 ) {
72 
73     interface ScreenshotViewCallback {
74         fun onUserInteraction()
75 
76         fun onDismiss()
77 
78         /** DOWN motion event was observed outside of the touchable areas of this view. */
79         fun onTouchOutside()
80     }
81 
82     val view: ScreenshotShelfView =
83         LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView
84     val screenshotPreview: View
85     var packageName: String = ""
86     var callbacks: ScreenshotViewCallback? = null
87     var screenshot: ScreenshotData? = null
88         set(value) {
89             value?.let {
90                 viewModel.setScreenshotBitmap(it.bitmap)
91                 val badgeBg =
92                     AppCompatResources.getDrawable(context, R.drawable.overlay_badge_background)
93                 val user = it.userHandle
94                 if (badgeBg != null && user != null) {
95                     viewModel.setScreenshotBadge(
96                         context.packageManager.getUserBadgedIcon(badgeBg, user)
97                     )
98                 }
99             }
100             field = value
101         }
102 
103     val isAttachedToWindow
104         get() = view.isAttachedToWindow
105 
106     var isDismissing = false
107     var isPendingSharedTransition = false
108 
109     private val animationController = ScreenshotAnimationController(view, viewModel)
110     private var inputMonitor: InputMonitorCompat? = null
111     private var inputEventReceiver: InputChannelCompat.InputEventReceiver? = null
112 
113     init {
114         shelfViewBinder.bind(
115             view,
116             viewModel,
117             animationController,
118             LayoutInflater.from(context),
119             onDismissalRequested = { event, velocity -> requestDismissal(event, velocity) },
120             onUserInteraction = { callbacks?.onUserInteraction() },
121         )
122         view.updateInsets(windowManager.currentWindowMetrics.windowInsets)
123         addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
124         setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
125         debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" }
126         view.viewTreeObserver.addOnComputeInternalInsetsListener { info ->
127             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION)
128             info.touchableRegion.set(getTouchRegion())
129         }
130         screenshotPreview = view.screenshotPreview
131         thumbnailObserver.setViews(
132             view.blurredScreenshotPreview,
133             view.requireViewById(R.id.screenshot_preview_border),
134         )
135         view.addOnAttachStateChangeListener(
136             object : View.OnAttachStateChangeListener {
137                 override fun onViewAttachedToWindow(v: View) {
138                     startInputListening()
139                 }
140 
141                 override fun onViewDetachedFromWindow(v: View) {
142                     stopInputListening()
143                 }
144             }
145         )
146     }
147 
148     fun reset() {
149         animationController.cancel()
150         isPendingSharedTransition = false
151         viewModel.reset()
152     }
153 
154     fun updateInsets(insets: WindowInsets) {
155         view.updateInsets(insets)
156     }
157 
158     fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator {
159         val entrance =
160             animationController.getEntranceAnimation(screenRect, showFlash) {
161                 viewModel.setAnimationState(AnimationState.ENTRANCE_REVEAL)
162             }
163         entrance.doOnStart {
164             thumbnailObserver.onEntranceStarted()
165             viewModel.setAnimationState(AnimationState.ENTRANCE_STARTED)
166         }
167         entrance.doOnEnd {
168             // reset the timeout when animation finishes
169             callbacks?.onUserInteraction()
170             thumbnailObserver.onEntranceComplete()
171             viewModel.setAnimationState(AnimationState.ENTRANCE_COMPLETE)
172         }
173         return entrance
174     }
175 
176     fun requestDismissal(event: ScreenshotEvent?) {
177         requestDismissal(event, null)
178     }
179 
180     private fun requestDismissal(event: ScreenshotEvent?, velocity: Float?) {
181         debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" }
182 
183         // If we're already animating out, don't restart the animation
184         if (isDismissing) {
185             debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" }
186             return
187         }
188         event?.let { logger.log(it, 0, packageName) }
189         val animator = animationController.getSwipeDismissAnimation(velocity)
190         animator.addListener(
191             object : AnimatorListenerAdapter() {
192                 override fun onAnimationStart(animator: Animator) {
193                     isDismissing = true
194                 }
195 
196                 override fun onAnimationEnd(animator: Animator) {
197                     isDismissing = false
198                     callbacks?.onDismiss()
199                 }
200             }
201         )
202         animator.start()
203     }
204 
205     fun prepareScrollingTransition(
206         response: ScrollCaptureResponse,
207         newScreenshot: Bitmap,
208         screenshotTakenInPortrait: Boolean,
209         onTransitionPrepared: Runnable,
210     ) {
211         viewModel.setScrollingScrimBitmap(newScreenshot)
212         viewModel.setScrollableRect(scrollableAreaOnScreen(response))
213         animationController.fadeForLongScreenshotTransition()
214         view.post { onTransitionPrepared.run() }
215     }
216 
217     private fun scrollableAreaOnScreen(response: ScrollCaptureResponse): Rect {
218         val r = Rect(response.boundsInWindow)
219         val windowInScreen = response.windowBounds
220         r.offset(windowInScreen?.left ?: 0, windowInScreen?.top ?: 0)
221         r.intersect(
222             Rect(
223                 0,
224                 0,
225                 context.resources.displayMetrics.widthPixels,
226                 context.resources.displayMetrics.heightPixels,
227             )
228         )
229         return r
230     }
231 
232     fun startLongScreenshotTransition(
233         transitionDestination: Rect,
234         onTransitionEnd: Runnable,
235         longScreenshot: ScrollCaptureController.LongScreenshot,
236     ) {
237         val transitionAnimation =
238             animationController.runLongScreenshotTransition(
239                 transitionDestination,
240                 longScreenshot,
241                 onTransitionEnd,
242             )
243         transitionAnimation.doOnEnd { callbacks?.onDismiss() }
244         transitionAnimation.start()
245     }
246 
247     fun restoreNonScrollingUi() {
248         viewModel.setScrollableRect(null)
249         viewModel.setScrollingScrimBitmap(null)
250         animationController.restoreUI()
251         callbacks?.onUserInteraction() // reset the timeout
252     }
253 
254     fun stopInputListening() {
255         inputMonitor?.dispose()
256         inputMonitor = null
257         inputEventReceiver?.dispose()
258         inputEventReceiver = null
259     }
260 
261     fun requestFocus() {
262         view.requestFocus()
263     }
264 
265     fun announceForAccessibility(string: String) = view.announceForAccessibility(string)
266 
267     fun prepareEntranceAnimation(runnable: Runnable) {
268         view.viewTreeObserver.addOnPreDrawListener(
269             object : ViewTreeObserver.OnPreDrawListener {
270                 override fun onPreDraw(): Boolean {
271                     debugLog(DEBUG_WINDOW) { "onPreDraw: startAnimation" }
272                     view.viewTreeObserver.removeOnPreDrawListener(this)
273                     runnable.run()
274                     return true
275                 }
276             }
277         )
278     }
279 
280     fun fadeForSharedTransition() {
281         animationController.fadeForSharedTransition()
282     }
283 
284     private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
285         val onBackInvokedCallback = OnBackInvokedCallback {
286             debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" }
287             onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
288         }
289         view.addOnAttachStateChangeListener(
290             object : View.OnAttachStateChangeListener {
291                 override fun onViewAttachedToWindow(v: View) {
292                     debugLog(DEBUG_INPUT) { "Registering Predictive Back callback" }
293                     view
294                         .findOnBackInvokedDispatcher()
295                         ?.registerOnBackInvokedCallback(
296                             OnBackInvokedDispatcher.PRIORITY_DEFAULT,
297                             onBackInvokedCallback,
298                         )
299                 }
300 
301                 override fun onViewDetachedFromWindow(view: View) {
302                     debugLog(DEBUG_INPUT) { "Unregistering Predictive Back callback" }
303                     view
304                         .findOnBackInvokedDispatcher()
305                         ?.unregisterOnBackInvokedCallback(onBackInvokedCallback)
306                 }
307             }
308         )
309     }
310 
311     private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
312         view.setOnKeyListener(
313             object : View.OnKeyListener {
314                 override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean {
315                     if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
316                         debugLog(DEBUG_INPUT) { "onKeyEvent: $keyCode" }
317                         onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
318                         return true
319                     }
320                     return false
321                 }
322             }
323         )
324     }
325 
326     private fun startInputListening() {
327         stopInputListening()
328         inputMonitor =
329             InputMonitorCompat("Screenshot", displayId).also {
330                 inputEventReceiver =
331                     it.getInputReceiver(Looper.getMainLooper(), Choreographer.getInstance()) {
332                         ev: InputEvent? ->
333                         if (
334                             ev is MotionEvent &&
335                                 ev.actionMasked == MotionEvent.ACTION_DOWN &&
336                                 !getTouchRegion().contains(ev.rawX.toInt(), ev.rawY.toInt())
337                         ) {
338                             callbacks?.onTouchOutside()
339                         }
340                     }
341             }
342     }
343 
344     private fun getTouchRegion(): Region {
345         return view.getTouchRegion(
346             windowManager.currentWindowMetrics.windowInsets.getInsets(
347                 WindowInsets.Type.systemGestures()
348             )
349         )
350     }
351 
352     @AssistedFactory
353     interface Factory {
354         fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
355     }
356 }
357