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