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