1 /*
2  * Copyright (C) 2021 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.statusbar.events
18 
19 import android.content.Context
20 import android.graphics.Rect
21 import android.view.ContextThemeWrapper
22 import android.view.Gravity
23 import android.view.LayoutInflater
24 import android.view.View
25 import android.view.View.MeasureSpec.AT_MOST
26 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
27 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
28 import android.widget.FrameLayout
29 import androidx.core.animation.Animator
30 import androidx.core.animation.AnimatorListenerAdapter
31 import androidx.core.animation.AnimatorSet
32 import androidx.core.animation.ValueAnimator
33 import com.android.internal.annotations.VisibleForTesting
34 import com.android.systemui.R
35 import com.android.systemui.flags.FeatureFlags
36 import com.android.systemui.flags.Flags
37 import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
38 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
39 import com.android.systemui.statusbar.window.StatusBarWindowController
40 import com.android.systemui.util.animation.AnimationUtil.Companion.frames
41 import javax.inject.Inject
42 import kotlin.math.roundToInt
43 
44 /**
45  * Controls the view for system event animations.
46  */
47 class SystemEventChipAnimationController @Inject constructor(
48     private val context: Context,
49     private val statusBarWindowController: StatusBarWindowController,
50     private val contentInsetsProvider: StatusBarContentInsetsProvider,
51     private val featureFlags: FeatureFlags,
52 ) : SystemStatusAnimationCallback {
53 
54     private lateinit var animationWindowView: FrameLayout
55     private lateinit var themedContext: ContextThemeWrapper
56 
57     private var currentAnimatedView: BackgroundAnimatableView? = null
58 
59     // Left for LTR, Right for RTL
60     private var animationDirection = LEFT
61 
62     @VisibleForTesting var chipBounds = Rect()
63     private val chipWidth get() = chipBounds.width()
64     private val chipRight get() = chipBounds.right
65     private val chipLeft get() = chipBounds.left
66     private var chipMinWidth = context.resources.getDimensionPixelSize(
67             R.dimen.ongoing_appops_chip_min_animation_width)
68 
69     private val dotSize = context.resources.getDimensionPixelSize(
70             R.dimen.ongoing_appops_dot_diameter)
71     // Use during animation so that multiple animators can update the drawing rect
72     private var animRect = Rect()
73 
74     // TODO: move to dagger
75     @VisibleForTesting var initialized = false
76 
77     /**
78      * Give the chip controller a chance to inflate and configure the chip view before we start
79      * animating
80      */
81     fun prepareChipAnimation(viewCreator: ViewCreator) {
82         if (!initialized) {
83             init()
84         }
85         animationDirection = if (animationWindowView.isLayoutRtl) RIGHT else LEFT
86 
87         // Initialize the animated view
88         val insets = contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()
89         currentAnimatedView = viewCreator(themedContext).also {
90             animationWindowView.addView(
91                     it.view,
92                     layoutParamsDefault(
93                             if (animationWindowView.isLayoutRtl) insets.first
94                             else insets.second))
95             it.view.alpha = 0f
96             // For some reason, the window view's measured width is always 0 here, so use the
97             // parent (status bar)
98             it.view.measure(
99                     View.MeasureSpec.makeMeasureSpec(
100                             (animationWindowView.parent as View).width, AT_MOST),
101                     View.MeasureSpec.makeMeasureSpec(
102                             (animationWindowView.parent as View).height, AT_MOST))
103 
104             updateChipBounds(it, contentInsetsProvider.getStatusBarContentAreaForCurrentRotation())
105         }
106     }
107 
108     override fun onSystemEventAnimationBegin(): Animator {
109         initializeAnimRect()
110 
111         val alphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
112             startDelay = 7.frames
113             duration = 5.frames
114             interpolator = null
115             addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
116         }
117         currentAnimatedView?.contentView?.alpha = 0f
118         val contentAlphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
119             startDelay = 10.frames
120             duration = 10.frames
121             interpolator = null
122             addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
123         }
124         val moveIn = ValueAnimator.ofInt(chipMinWidth, chipWidth).apply {
125             startDelay = 7.frames
126             duration = 23.frames
127             interpolator = STATUS_BAR_X_MOVE_IN
128             addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
129         }
130         val animSet = AnimatorSet()
131         animSet.playTogether(alphaIn, contentAlphaIn, moveIn)
132         return animSet
133     }
134 
135     override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator {
136         initializeAnimRect()
137         val finish = if (hasPersistentDot) {
138             createMoveOutAnimationForDot()
139         } else {
140             createMoveOutAnimationDefault()
141         }
142 
143         finish.addListener(object : AnimatorListenerAdapter() {
144             override fun onAnimationEnd(animation: Animator) {
145                 animationWindowView.removeView(currentAnimatedView!!.view)
146             }
147         })
148 
149         return finish
150     }
151 
152     private fun createMoveOutAnimationForDot(): Animator {
153         val width1 = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
154             duration = 9.frames
155             interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1
156             addUpdateListener {
157                 updateAnimatedViewBoundsWidth(animatedValue as Int)
158             }
159         }
160 
161         val width2 = ValueAnimator.ofInt(chipMinWidth, dotSize).apply {
162             startDelay = 9.frames
163             duration = 20.frames
164             interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2
165             addUpdateListener {
166                 updateAnimatedViewBoundsWidth(animatedValue as Int)
167             }
168         }
169 
170         val keyFrame1Height = dotSize * 2
171         val chipVerticalCenter = chipBounds.top + chipBounds.height() / 2
172         val height1 = ValueAnimator.ofInt(chipBounds.height(), keyFrame1Height).apply {
173             startDelay = 8.frames
174             duration = 6.frames
175             interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1
176             addUpdateListener {
177                 updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
178             }
179         }
180 
181         val height2 = ValueAnimator.ofInt(keyFrame1Height, dotSize).apply {
182             startDelay = 14.frames
183             duration = 15.frames
184             interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2
185             addUpdateListener {
186                 updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
187             }
188         }
189 
190         // Move the chip view to overlap exactly with the privacy dot. The chip displays by default
191         // exactly adjacent to the dot, so we can just move over by the diameter of the dot itself
192         val moveOut = ValueAnimator.ofInt(0, dotSize).apply {
193             startDelay = 3.frames
194             duration = 11.frames
195             interpolator = STATUS_CHIP_MOVE_TO_DOT
196             addUpdateListener {
197                 // If RTL, we can just invert the move
198                 val amt = if (animationDirection == LEFT) {
199                         animatedValue as Int
200                 } else {
201                     -(animatedValue as Int)
202                 }
203                 updateAnimatedBoundsX(amt)
204             }
205         }
206 
207         val animSet = AnimatorSet()
208         animSet.playTogether(width1, width2, height1, height2, moveOut)
209         return animSet
210     }
211 
212     private fun createMoveOutAnimationDefault(): Animator {
213         val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
214             startDelay = 6.frames
215             duration = 6.frames
216             interpolator = null
217             addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
218         }
219 
220         val contentAlphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
221             duration = 5.frames
222             interpolator = null
223             addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
224         }
225 
226         val moveOut = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
227             duration = 23.frames
228             interpolator = STATUS_BAR_X_MOVE_OUT
229             addUpdateListener {
230                 currentAnimatedView?.apply {
231                     updateAnimatedViewBoundsWidth(animatedValue as Int)
232                 }
233             }
234         }
235 
236         val animSet = AnimatorSet()
237         animSet.playTogether(alphaOut, contentAlphaOut, moveOut)
238         return animSet
239     }
240 
241     fun init() {
242         initialized = true
243         themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
244         animationWindowView = LayoutInflater.from(themedContext)
245                 .inflate(R.layout.system_event_animation_window, null) as FrameLayout
246         // Matches status_bar.xml
247         val height = themedContext.resources.getDimensionPixelSize(R.dimen.status_bar_height)
248         val lp = FrameLayout.LayoutParams(MATCH_PARENT, height)
249         lp.gravity = Gravity.END or Gravity.TOP
250         statusBarWindowController.addViewToWindow(animationWindowView, lp)
251         animationWindowView.clipToPadding = false
252         animationWindowView.clipChildren = false
253 
254         // Use contentInsetsProvider rather than configuration controller, since we only care
255         // about status bar dimens
256         contentInsetsProvider.addCallback(object : StatusBarContentInsetsChangedListener {
257             override fun onStatusBarContentInsetsChanged() {
258                 val newContentArea = contentInsetsProvider
259                     .getStatusBarContentAreaForCurrentRotation()
260                 updateDimens(newContentArea)
261 
262                 // If we are currently animating, we have to re-solve for the chip bounds. If we're
263                 // not animating then [prepareChipAnimation] will take care of it for us
264                 currentAnimatedView?.let {
265                     updateChipBounds(it, newContentArea)
266                     // Since updateCurrentAnimatedView can only be called during an animation, we
267                     // have to create a dummy animator here to apply the new chip bounds
268                     val animator = ValueAnimator.ofInt(0, 1).setDuration(0)
269                     animator.addUpdateListener { updateCurrentAnimatedView() }
270                     animator.start()
271                 }
272             }
273         })
274     }
275 
276     private fun updateDimens(contentArea: Rect) {
277         val lp = animationWindowView.layoutParams as FrameLayout.LayoutParams
278         lp.height = contentArea.height()
279 
280         animationWindowView.layoutParams = lp
281     }
282 
283     /**
284      * Use the current status bar content area and the current chip's measured size to update
285      * the animation rect and chipBounds. This method can be called at any time and will update
286      * the current animation values properly during e.g. a rotation.
287      */
288     private fun updateChipBounds(chip: BackgroundAnimatableView, contentArea: Rect) {
289         // decide which direction we're animating from, and then set some screen coordinates
290         val chipTop = (contentArea.bottom - chip.view.measuredHeight) / 2
291         val chipBottom = chipTop + chip.view.measuredHeight
292         val chipRight: Int
293         val chipLeft: Int
294 
295         when (animationDirection) {
296             LEFT -> {
297                 chipRight = contentArea.right
298                 chipLeft = contentArea.right - chip.chipWidth
299             }
300             else /* RIGHT */ -> {
301                 chipLeft = contentArea.left
302                 chipRight = contentArea.left + chip.chipWidth
303             }
304         }
305         chipBounds = Rect(chipLeft, chipTop, chipRight, chipBottom)
306         animRect.set(chipBounds)
307     }
308 
309     private fun layoutParamsDefault(marginEnd: Int): FrameLayout.LayoutParams =
310             FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).also {
311                 it.gravity = Gravity.END or Gravity.CENTER_VERTICAL
312                 it.marginEnd = marginEnd
313             }
314 
315     private fun initializeAnimRect() = if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) {
316         animRect.set(chipBounds)
317     } else {
318         animRect.set(
319                 chipLeft,
320                 currentAnimatedView!!.view.top,
321                 chipRight,
322                 currentAnimatedView!!.view.bottom)
323     }
324 
325     /**
326      * To be called during an animation, sets the width and updates the current animated chip view
327      */
328     private fun updateAnimatedViewBoundsWidth(width: Int) {
329         when (animationDirection) {
330             LEFT -> {
331                 animRect.set((chipRight - width), animRect.top, chipRight, animRect.bottom)
332             } else /* RIGHT */ -> {
333                 animRect.set(chipLeft, animRect.top, (chipLeft + width), animRect.bottom)
334             }
335         }
336 
337         updateCurrentAnimatedView()
338     }
339 
340     /**
341      * To be called during an animation, updates the animation rect and sends the update to the chip
342      */
343     private fun updateAnimatedViewBoundsHeight(height: Int, verticalCenter: Int) {
344         animRect.set(
345                 animRect.left,
346                 verticalCenter - (height.toFloat() / 2).roundToInt(),
347                 animRect.right,
348                 verticalCenter + (height.toFloat() / 2).roundToInt())
349 
350         updateCurrentAnimatedView()
351     }
352 
353     /**
354      * To be called during an animation, updates the animation rect offset and updates the chip
355      */
356     private fun updateAnimatedBoundsX(translation: Int) {
357         currentAnimatedView?.view?.translationX = translation.toFloat()
358     }
359 
360     /**
361      * To be called during an animation. Sets the chip rect to animRect
362      */
363     private fun updateCurrentAnimatedView() {
364         currentAnimatedView?.setBoundsForAnimation(
365                 animRect.left, animRect.top, animRect.right, animRect.bottom
366         )
367     }
368 }
369 
370 /**
371  * Chips should provide a view that can be animated with something better than a fade-in
372  */
373 interface BackgroundAnimatableView {
374     val view: View // Since this can't extend View, add a view prop
375         get() = this as View
376     val contentView: View? // This will be alpha faded during appear and disappear animation
377         get() = null
378     val chipWidth: Int
379         get() = view.measuredWidth
380     fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int)
381 }
382 
383 // Animation directions
384 private const val LEFT = 1
385 private const val RIGHT = 2
386