• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.media.taptotransfer.receiver
18 
19 import android.animation.TimeInterpolator
20 import android.annotation.SuppressLint
21 import android.animation.ValueAnimator
22 import android.app.StatusBarManager
23 import android.content.Context
24 import android.graphics.Rect
25 import android.graphics.drawable.Drawable
26 import android.graphics.drawable.Icon
27 import android.media.MediaRoute2Info
28 import android.os.Handler
29 import android.os.PowerManager
30 import android.view.Gravity
31 import android.view.View
32 import android.view.ViewGroup
33 import android.view.WindowManager
34 import android.view.accessibility.AccessibilityManager
35 import android.view.View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE
36 import android.view.View.ACCESSIBILITY_LIVE_REGION_NONE
37 import com.android.internal.widget.CachingIconView
38 import com.android.systemui.R
39 import com.android.systemui.animation.Interpolators
40 import com.android.systemui.common.shared.model.ContentDescription
41 import com.android.systemui.common.ui.binder.TintedIconViewBinder
42 import com.android.systemui.dagger.SysUISingleton
43 import com.android.systemui.dagger.qualifiers.Main
44 import com.android.systemui.dump.DumpManager
45 import com.android.systemui.media.taptotransfer.MediaTttFlags
46 import com.android.systemui.media.taptotransfer.common.MediaTttIcon
47 import com.android.systemui.media.taptotransfer.common.MediaTttUtils
48 import com.android.systemui.statusbar.CommandQueue
49 import com.android.systemui.statusbar.policy.ConfigurationController
50 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
51 import com.android.systemui.temporarydisplay.TemporaryViewInfo
52 import com.android.systemui.temporarydisplay.ViewPriority
53 import com.android.systemui.util.animation.AnimationUtil.Companion.frames
54 import com.android.systemui.util.concurrency.DelayableExecutor
55 import com.android.systemui.util.time.SystemClock
56 import com.android.systemui.util.view.ViewUtil
57 import com.android.systemui.util.wakelock.WakeLock
58 import javax.inject.Inject
59 
60 /**
61  * A controller to display and hide the Media Tap-To-Transfer chip on the **receiving** device.
62  *
63  * This chip is shown when a user is transferring media to/from a sending device and this device.
64  *
65  * TODO(b/245610654): Re-name this to be MediaTttReceiverCoordinator.
66  */
67 @SysUISingleton
68 open class MediaTttChipControllerReceiver @Inject constructor(
69         private val commandQueue: CommandQueue,
70         context: Context,
71         logger: MediaTttReceiverLogger,
72         windowManager: WindowManager,
73         mainExecutor: DelayableExecutor,
74         accessibilityManager: AccessibilityManager,
75         configurationController: ConfigurationController,
76         dumpManager: DumpManager,
77         powerManager: PowerManager,
78         @Main private val mainHandler: Handler,
79         private val mediaTttFlags: MediaTttFlags,
80         private val uiEventLogger: MediaTttReceiverUiEventLogger,
81         private val viewUtil: ViewUtil,
82         wakeLockBuilder: WakeLock.Builder,
83         systemClock: SystemClock,
84         private val rippleController: MediaTttReceiverRippleController,
85 ) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttReceiverLogger>(
86         context,
87         logger,
88         windowManager,
89         mainExecutor,
90         accessibilityManager,
91         configurationController,
92         dumpManager,
93         powerManager,
94         R.layout.media_ttt_chip_receiver,
95         wakeLockBuilder,
96         systemClock,
97 ) {
98     @SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
99     override val windowLayoutParams = commonWindowLayoutParams.apply {
100         gravity = Gravity.BOTTOM.or(Gravity.CENTER_HORIZONTAL)
101         // Params below are needed for the ripple to work correctly
102         width = WindowManager.LayoutParams.MATCH_PARENT
103         height = WindowManager.LayoutParams.MATCH_PARENT
104         layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
105         fitInsetsTypes = 0 // Ignore insets from all system bars
106     }
107 
108     // Value animator that controls the bouncing animation of views.
109     private val bounceAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
110         repeatCount = ValueAnimator.INFINITE
111         repeatMode = ValueAnimator.REVERSE
112         duration = ICON_BOUNCE_ANIM_DURATION
113     }
114 
115     private val commandQueueCallbacks = object : CommandQueue.Callbacks {
116         override fun updateMediaTapToTransferReceiverDisplay(
117             @StatusBarManager.MediaTransferReceiverState displayState: Int,
118             routeInfo: MediaRoute2Info,
119             appIcon: Icon?,
120             appName: CharSequence?
121         ) {
122             this@MediaTttChipControllerReceiver.updateMediaTapToTransferReceiverDisplay(
123                 displayState, routeInfo, appIcon, appName
124             )
125         }
126     }
127 
128     private fun updateMediaTapToTransferReceiverDisplay(
129         @StatusBarManager.MediaTransferReceiverState displayState: Int,
130         routeInfo: MediaRoute2Info,
131         appIcon: Icon?,
132         appName: CharSequence?
133     ) {
134         val chipState: ChipStateReceiver? = ChipStateReceiver.getReceiverStateFromId(displayState)
135         val stateName = chipState?.name ?: "Invalid"
136         logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
137 
138         if (chipState == null) {
139             logger.logStateChangeError(displayState)
140             return
141         }
142         uiEventLogger.logReceiverStateChange(chipState)
143 
144         if (chipState != ChipStateReceiver.CLOSE_TO_SENDER) {
145             removeView(routeInfo.id, removalReason = chipState.name)
146             return
147         }
148         if (appIcon == null) {
149             displayView(
150                 ChipReceiverInfo(
151                     routeInfo,
152                     appIconDrawableOverride = null,
153                     appName,
154                     id = routeInfo.id,
155                 )
156             )
157             return
158         }
159 
160         appIcon.loadDrawableAsync(
161                 context,
162                 Icon.OnDrawableLoadedListener { drawable ->
163                     displayView(
164                         ChipReceiverInfo(
165                             routeInfo,
166                             drawable,
167                             appName,
168                             id = routeInfo.id,
169                         )
170                     )
171                 },
172                 // Notify the listener on the main handler since the listener will update
173                 // the UI.
174                 mainHandler
175         )
176     }
177 
178     override fun start() {
179         super.start()
180         if (mediaTttFlags.isMediaTttEnabled()) {
181             commandQueue.addCallback(commandQueueCallbacks)
182         }
183     }
184 
185     override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) {
186         val packageName = newInfo.routeInfo.clientPackageName
187         var iconInfo = MediaTttUtils.getIconInfoFromPackageName(
188             context,
189             packageName,
190             isReceiver = true,
191         ) {
192             logger.logPackageNotFound(packageName)
193         }
194 
195         if (newInfo.appNameOverride != null) {
196             iconInfo = iconInfo.copy(
197                 contentDescription = ContentDescription.Loaded(newInfo.appNameOverride.toString())
198             )
199         }
200 
201         if (newInfo.appIconDrawableOverride != null) {
202             iconInfo = iconInfo.copy(
203                 icon = MediaTttIcon.Loaded(newInfo.appIconDrawableOverride),
204                 isAppIcon = true,
205             )
206         }
207 
208         val iconPadding =
209             if (iconInfo.isAppIcon) {
210                 0
211             } else {
212                 context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding)
213             }
214 
215         val iconView = currentView.getAppIconView()
216         iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding)
217         TintedIconViewBinder.bind(iconInfo.toTintedIcon(), iconView)
218 
219         val iconContainerView = currentView.getIconContainerView()
220         iconContainerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE
221     }
222 
223     override fun animateViewIn(view: ViewGroup) {
224         val iconContainerView = view.getIconContainerView()
225         val iconRippleView: ReceiverChipRippleView = view.requireViewById(R.id.icon_glow_ripple)
226         val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
227         val translationYBy = getTranslationAmount()
228         // Expand ripple before translating icon container to make sure both views have same bounds.
229         rippleController.expandToInProgressState(rippleView, iconRippleView)
230         // Make the icon container view starts animation from bottom of the screen.
231         iconContainerView.translationY = rippleController.getReceiverIconSize().toFloat()
232         animateViewTranslationAndFade(
233             iconContainerView,
234             translationYBy = -1 * translationYBy,
235             alphaEndValue = 1f,
236             Interpolators.EMPHASIZED_DECELERATE,
237         ) {
238             animateBouncingView(iconContainerView, translationYBy * BOUNCE_TRANSLATION_RATIO)
239         }
240     }
241 
242     override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
243         val iconContainerView = view.getIconContainerView()
244         val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
245         val translationYBy = getTranslationAmount()
246 
247         // Remove update listeners from bounce animator to prevent any conflict with
248         // translation animation.
249         bounceAnimator.removeAllUpdateListeners()
250         bounceAnimator.cancel()
251         if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name &&
252                 mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) {
253             rippleController.expandToSuccessState(rippleView, onAnimationEnd)
254             animateViewTranslationAndFade(
255                 iconContainerView,
256                 -1 * translationYBy,
257                 0f,
258                 translationDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
259                 alphaDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
260             )
261         } else {
262             rippleController.collapseRipple(rippleView, onAnimationEnd)
263             animateViewTranslationAndFade(iconContainerView, translationYBy, 0f)
264         }
265     }
266 
267     override fun getTouchableRegion(view: View, outRect: Rect) {
268         // Even though the app icon view isn't touchable, users might think it is. So, use it as the
269         // touchable region to ensure that touches don't get passed to the window below.
270         viewUtil.setRectToViewWindowLocation(view.getAppIconView(), outRect)
271     }
272 
273     /** Animation of view translation and fading. */
274     private fun animateViewTranslationAndFade(
275         view: ViewGroup,
276         translationYBy: Float,
277         alphaEndValue: Float,
278         interpolator: TimeInterpolator? = null,
279         translationDuration: Long = ICON_TRANSLATION_ANIM_DURATION,
280         alphaDuration: Long = ICON_ALPHA_ANIM_DURATION,
281         onAnimationEnd: Runnable? = null,
282     ) {
283         view.animate()
284             .translationYBy(translationYBy)
285             .setInterpolator(interpolator)
286             .setDuration(translationDuration)
287             .withEndAction { onAnimationEnd?.run() }
288             .start()
289         view.animate()
290             .alpha(alphaEndValue)
291             .setDuration(alphaDuration)
292             .start()
293     }
294 
295     /** Returns the amount that the chip will be translated by in its intro animation. */
296     private fun getTranslationAmount(): Float {
297         return rippleController.getReceiverIconSize() * 2f
298     }
299 
300     private fun View.getAppIconView(): CachingIconView {
301         return this.requireViewById(R.id.app_icon)
302     }
303 
304     private fun View.getIconContainerView(): ViewGroup {
305         return this.requireViewById(R.id.icon_container_view)
306     }
307 
308     private fun animateBouncingView(iconContainerView: ViewGroup, translationYBy: Float) {
309         if (bounceAnimator.isStarted) {
310             return
311         }
312 
313         addViewToBounceAnimation(iconContainerView, translationYBy)
314 
315         // In order not to announce description every time the view animate.
316         iconContainerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
317         bounceAnimator.start()
318     }
319 
320     private fun addViewToBounceAnimation(view: View, translationYBy: Float) {
321         val prevTranslationY = view.translationY
322         bounceAnimator.addUpdateListener { updateListener ->
323             val progress = updateListener.animatedValue as Float
324             view.translationY = prevTranslationY + translationYBy * progress
325         }
326     }
327 
328     companion object {
329         private const val ICON_TRANSLATION_ANIM_DURATION = 500L
330         private const val ICON_BOUNCE_ANIM_DURATION = 750L
331         private const val ICON_TRANSLATION_SUCCEEDED_DURATION = 167L
332         private const val BOUNCE_TRANSLATION_RATIO = 0.15f
333         private val ICON_ALPHA_ANIM_DURATION = 5.frames
334     }
335 }
336 
337 data class ChipReceiverInfo(
338     val routeInfo: MediaRoute2Info,
339     val appIconDrawableOverride: Drawable?,
340     val appNameOverride: CharSequence?,
341     override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER,
342     override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER,
343     override val id: String,
344     override val priority: ViewPriority = ViewPriority.NORMAL,
345 ) : TemporaryViewInfo()
346