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