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