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