1 package com.android.systemui.screenshot
2 
3 import android.animation.Animator
4 import android.animation.AnimatorListenerAdapter
5 import android.animation.ValueAnimator
6 import android.os.UserHandle
7 import android.view.View
8 import android.view.ViewGroup
9 import android.view.ViewGroup.MarginLayoutParams
10 import android.view.ViewTreeObserver
11 import android.view.animation.AccelerateDecelerateInterpolator
12 import androidx.constraintlayout.widget.Guideline
13 import com.android.systemui.R
14 import com.android.systemui.flags.FeatureFlags
15 import com.android.systemui.flags.Flags
16 import javax.inject.Inject
17 
18 /**
19  * MessageContainerController controls the display of content in the screenshot message container.
20  */
21 class MessageContainerController
22 @Inject
23 constructor(
24     private val workProfileMessageController: WorkProfileMessageController,
25     private val screenshotDetectionController: ScreenshotDetectionController,
26     private val featureFlags: FeatureFlags,
27 ) {
28     private lateinit var container: ViewGroup
29     private lateinit var guideline: Guideline
30     private lateinit var workProfileFirstRunView: ViewGroup
31     private lateinit var detectionNoticeView: ViewGroup
32     private var animateOut: Animator? = null
33 
34     fun setView(screenshotView: ViewGroup) {
35         container = screenshotView.requireViewById(R.id.screenshot_message_container)
36         guideline = screenshotView.requireViewById(R.id.guideline)
37 
38         workProfileFirstRunView = container.requireViewById(R.id.work_profile_first_run)
39         detectionNoticeView = container.requireViewById(R.id.screenshot_detection_notice)
40 
41         // Restore to starting state.
42         container.visibility = View.GONE
43         guideline.setGuidelineEnd(0)
44         workProfileFirstRunView.visibility = View.GONE
45         detectionNoticeView.visibility = View.GONE
46     }
47 
48     // Minimal implementation for use when Flags.SCREENSHOT_METADATA isn't turned on.
49     fun onScreenshotTaken(userHandle: UserHandle) {
50         val workProfileData = workProfileMessageController.onScreenshotTaken(userHandle)
51         if (workProfileData != null) {
52             workProfileFirstRunView.visibility = View.VISIBLE
53             detectionNoticeView.visibility = View.GONE
54 
55             workProfileMessageController.populateView(
56                 workProfileFirstRunView,
57                 workProfileData,
58                 this::animateOutMessageContainer
59             )
60             animateInMessageContainer()
61         }
62     }
63 
64     fun onScreenshotTaken(screenshot: ScreenshotData) {
65         val workProfileData = workProfileMessageController.onScreenshotTaken(screenshot.userHandle)
66         var notifiedApps: List<CharSequence> = listOf()
67         if (featureFlags.isEnabled(Flags.SCREENSHOT_DETECTION)) {
68             notifiedApps = screenshotDetectionController.maybeNotifyOfScreenshot(screenshot)
69         }
70 
71         // If work profile first run needs to show, bias towards that, otherwise show screenshot
72         // detection notification if needed.
73         if (workProfileData != null) {
74             workProfileFirstRunView.visibility = View.VISIBLE
75             detectionNoticeView.visibility = View.GONE
76             workProfileMessageController.populateView(
77                 workProfileFirstRunView,
78                 workProfileData,
79                 this::animateOutMessageContainer
80             )
81             animateInMessageContainer()
82         } else if (notifiedApps.isNotEmpty()) {
83             detectionNoticeView.visibility = View.VISIBLE
84             workProfileFirstRunView.visibility = View.GONE
85             screenshotDetectionController.populateView(detectionNoticeView, notifiedApps)
86             animateInMessageContainer()
87         }
88     }
89 
90     private fun animateInMessageContainer() {
91         if (container.visibility == View.VISIBLE) return
92 
93         // Need the container to be fully measured before animating in (to know animation offset
94         // destination)
95         container.visibility = View.VISIBLE
96         container.viewTreeObserver.addOnPreDrawListener(
97             object : ViewTreeObserver.OnPreDrawListener {
98                 override fun onPreDraw(): Boolean {
99                     container.viewTreeObserver.removeOnPreDrawListener(this)
100                     getAnimator(true).start()
101                     return false
102                 }
103             }
104         )
105     }
106 
107     private fun animateOutMessageContainer() {
108         if (animateOut != null) return
109 
110         animateOut =
111             getAnimator(false).apply {
112                 addListener(
113                     object : AnimatorListenerAdapter() {
114                         override fun onAnimationEnd(animation: Animator) {
115                             super.onAnimationEnd(animation)
116                             container.visibility = View.GONE
117                             animateOut = null
118                         }
119                     }
120                 )
121                 start()
122             }
123     }
124 
125     private fun getAnimator(animateIn: Boolean): Animator {
126         val params = container.layoutParams as MarginLayoutParams
127         val offset = container.height + params.topMargin + params.bottomMargin
128         val anim = if (animateIn) ValueAnimator.ofFloat(0f, 1f) else ValueAnimator.ofFloat(1f, 0f)
129         with(anim) {
130             duration = ScreenshotView.SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS
131             interpolator = AccelerateDecelerateInterpolator()
132             addUpdateListener { valueAnimator: ValueAnimator ->
133                 val interpolation = valueAnimator.animatedValue as Float
134                 guideline.setGuidelineEnd((interpolation * offset).toInt())
135                 container.alpha = interpolation
136             }
137         }
138         return anim
139     }
140 }
141