1 /* <lambda>null2 * Copyright (C) 2023 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.pipeline.shared.ui.binder 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.view.View 22 import androidx.core.view.isVisible 23 import androidx.lifecycle.Lifecycle 24 import androidx.lifecycle.lifecycleScope 25 import androidx.lifecycle.repeatOnLifecycle 26 import com.android.app.animation.Interpolators 27 import com.android.systemui.dagger.SysUISingleton 28 import com.android.systemui.lifecycle.repeatWhenAttached 29 import com.android.systemui.res.R 30 import com.android.systemui.scene.shared.flag.SceneContainerFlag 31 import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel 32 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips 33 import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipBinder 34 import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipViewBinding 35 import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModelLegacy 36 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel 37 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays 38 import com.android.systemui.statusbar.core.StatusBarRootModernization 39 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState 40 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn 41 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut 42 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.RunningChipAnim 43 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ConnectedDisplaysStatusBarNotificationIconViewStore 44 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor 45 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment 46 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization 47 import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel 48 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel 49 import javax.inject.Inject 50 import kotlinx.coroutines.flow.combine 51 import kotlinx.coroutines.flow.distinctUntilChanged 52 import kotlinx.coroutines.launch 53 54 /** 55 * Interface to assist with binding the [CollapsedStatusBarFragment] to [HomeStatusBarViewModel]. 56 * Used only to enable easy testing of [CollapsedStatusBarFragment]. 57 */ 58 interface HomeStatusBarViewBinder { 59 /** 60 * Binds the view to the view-model. [listener] will be notified whenever an event that may 61 * change the status bar visibility occurs. 62 * 63 * Null chip animations are used when [StatusBarRootModernization] is off (i.e., when we are 64 * binding from the fragment). If non-null, they control the animation of the system icon area 65 * to support the chip animations. 66 */ 67 fun bind( 68 displayId: Int, 69 view: View, 70 viewModel: HomeStatusBarViewModel, 71 systemEventChipAnimateIn: ((View) -> Unit)?, 72 systemEventChipAnimateOut: ((View) -> Unit)?, 73 listener: StatusBarVisibilityChangeListener?, 74 ) 75 } 76 77 @SysUISingleton 78 class HomeStatusBarViewBinderImpl 79 @Inject 80 constructor( 81 private val viewStoreFactory: ConnectedDisplaysStatusBarNotificationIconViewStore.Factory 82 ) : HomeStatusBarViewBinder { bindnull83 override fun bind( 84 displayId: Int, 85 view: View, 86 viewModel: HomeStatusBarViewModel, 87 systemEventChipAnimateIn: ((View) -> Unit)?, 88 systemEventChipAnimateOut: ((View) -> Unit)?, 89 listener: StatusBarVisibilityChangeListener?, 90 ) { 91 // Set some top-level views to gone before we get started 92 val primaryChipView: View = view.requireViewById(R.id.ongoing_activity_chip_primary) 93 val systemInfoView = view.requireViewById<View>(R.id.status_bar_end_side_content) 94 val clockView = view.requireViewById<View>(R.id.clock) 95 val notificationIconsArea = view.requireViewById<View>(R.id.notificationIcons) 96 97 // CollapsedStatusBarFragment doesn't need this 98 if (StatusBarRootModernization.isEnabled) { 99 // GONE because this shouldn't take space in the layout 100 primaryChipView.hideInitially(state = View.GONE) 101 systemInfoView.hideInitially() 102 clockView.hideInitially() 103 notificationIconsArea.hideInitially() 104 } 105 106 view.repeatWhenAttached { 107 repeatOnLifecycle(Lifecycle.State.CREATED) { 108 val iconViewStore = 109 if (StatusBarConnectedDisplays.isEnabled) { 110 viewStoreFactory.create(displayId).also { 111 lifecycleScope.launch { it.activate() } 112 } 113 } else { 114 null 115 } 116 listener?.let { listener -> 117 launch { 118 viewModel.isTransitioningFromLockscreenToOccluded.collect { 119 listener.onStatusBarVisibilityMaybeChanged() 120 } 121 } 122 } 123 124 listener?.let { listener -> 125 launch { 126 viewModel.transitionFromLockscreenToDreamStartedEvent.collect { 127 listener.onTransitionFromLockscreenToDreamStarted() 128 } 129 } 130 } 131 132 if (NotificationsLiveDataStoreRefactor.isEnabled) { 133 val lightsOutView: View = view.requireViewById(R.id.notification_lights_out) 134 launch { 135 viewModel.areNotificationsLightsOut.collect { show -> 136 animateLightsOutView(lightsOutView, show) 137 } 138 } 139 } 140 141 if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) { 142 launch { 143 viewModel.mediaProjectionStopDialogDueToCallEndedState.collect { stopDialog 144 -> 145 if (stopDialog is MediaProjectionStopDialogModel.Shown) { 146 stopDialog.createAndShowDialog() 147 } 148 } 149 } 150 } 151 152 if (!StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled) { 153 val primaryChipViewBinding = 154 OngoingActivityChipBinder.createBinding(primaryChipView) 155 156 launch { 157 combine( 158 viewModel.primaryOngoingActivityChip, 159 viewModel.canShowOngoingActivityChips, 160 ::Pair, 161 ) 162 .distinctUntilChanged() 163 .collect { (primaryChipModel, areChipsAllowed) -> 164 OngoingActivityChipBinder.bind( 165 primaryChipModel, 166 primaryChipViewBinding, 167 iconViewStore, 168 ) 169 170 if (StatusBarRootModernization.isEnabled) { 171 bindLegacyPrimaryOngoingActivityChipWithVisibility( 172 areChipsAllowed, 173 primaryChipModel, 174 primaryChipViewBinding, 175 ) 176 } else { 177 when (primaryChipModel) { 178 is OngoingActivityChipModel.Active -> 179 listener?.onOngoingActivityStatusChanged( 180 hasPrimaryOngoingActivity = true, 181 hasSecondaryOngoingActivity = false, 182 shouldAnimate = true, 183 ) 184 185 is OngoingActivityChipModel.Inactive -> 186 listener?.onOngoingActivityStatusChanged( 187 hasPrimaryOngoingActivity = false, 188 hasSecondaryOngoingActivity = false, 189 shouldAnimate = primaryChipModel.shouldAnimate, 190 ) 191 } 192 } 193 } 194 } 195 } 196 197 if (StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled) { 198 // Create view bindings here so we don't keep re-fetching child views each time 199 // the chip model changes. 200 val primaryChipViewBinding = 201 OngoingActivityChipBinder.createBinding(primaryChipView) 202 val secondaryChipViewBinding = 203 OngoingActivityChipBinder.createBinding( 204 view.requireViewById(R.id.ongoing_activity_chip_secondary) 205 ) 206 OngoingActivityChipBinder.updateTypefaces(primaryChipViewBinding) 207 OngoingActivityChipBinder.updateTypefaces(secondaryChipViewBinding) 208 launch { 209 combine( 210 viewModel.ongoingActivityChipsLegacy, 211 viewModel.canShowOngoingActivityChips, 212 ::Pair, 213 ) 214 .distinctUntilChanged() 215 .collect { (chips, areChipsAllowed) -> 216 OngoingActivityChipBinder.bind( 217 chips.primary, 218 primaryChipViewBinding, 219 iconViewStore, 220 ) 221 OngoingActivityChipBinder.bind( 222 chips.secondary, 223 secondaryChipViewBinding, 224 iconViewStore, 225 ) 226 if (StatusBarRootModernization.isEnabled) { 227 bindOngoingActivityChipsWithVisibility( 228 areChipsAllowed, 229 chips, 230 primaryChipViewBinding, 231 secondaryChipViewBinding, 232 ) 233 } else { 234 listener?.onOngoingActivityStatusChanged( 235 hasPrimaryOngoingActivity = 236 chips.primary is OngoingActivityChipModel.Active, 237 hasSecondaryOngoingActivity = 238 chips.secondary is OngoingActivityChipModel.Active, 239 // TODO(b/364653005): Figure out the animation story here. 240 shouldAnimate = true, 241 ) 242 } 243 } 244 } 245 launch { 246 viewModel.contentArea.collect { _ -> 247 OngoingActivityChipBinder.resetPrimaryChipWidthRestrictions( 248 primaryChipViewBinding, 249 viewModel.ongoingActivityChipsLegacy.value.primary, 250 ) 251 OngoingActivityChipBinder.resetSecondaryChipWidthRestrictions( 252 secondaryChipViewBinding, 253 viewModel.ongoingActivityChipsLegacy.value.secondary, 254 ) 255 view.requestLayout() 256 } 257 } 258 } 259 260 if (SceneContainerFlag.isEnabled) { 261 listener?.let { listener -> 262 launch { 263 viewModel.isHomeStatusBarAllowed.collect { 264 listener.onIsHomeStatusBarAllowedBySceneChanged(it) 265 } 266 } 267 } 268 } 269 270 if (StatusBarRootModernization.isEnabled) { 271 // TODO(b/393445203): figure out the best story for this stub view. This crashes 272 // if we move it up to the top of [bind] 273 val operatorNameView = view.requireViewById<View>(R.id.operator_name_frame) 274 operatorNameView.isVisible = false 275 276 StatusBarOperatorNameViewBinder.bind( 277 operatorNameView, 278 viewModel.operatorNameViewModel, 279 viewModel.areaTint, 280 ) 281 launch { 282 viewModel.shouldShowOperatorNameView.collect { 283 operatorNameView.isVisible = it 284 } 285 } 286 287 launch { viewModel.isClockVisible.collect { clockView.adjustVisibility(it) } } 288 289 launch { 290 viewModel.isNotificationIconContainerVisible.collect { 291 notificationIconsArea.adjustVisibility(it) 292 } 293 } 294 295 launch { 296 viewModel.systemInfoCombinedVis.collect { (baseVis, animState) -> 297 // Broadly speaking, the baseVis controls the view.visibility, and 298 // the animation state uses only alpha to achieve its effect. This 299 // means that we can always modify the visibility, and if we're 300 // animating we can use the animState to handle it. If we are not 301 // animating, then we can use the baseVis default animation 302 if (animState.isAnimatingChip()) { 303 // Just apply the visibility of the view, but don't animate 304 systemInfoView.visibility = baseVis.visibility 305 // Now apply the animation state, with its animator 306 when (animState) { 307 AnimatingIn -> { 308 systemEventChipAnimateIn?.invoke(systemInfoView) 309 } 310 AnimatingOut -> { 311 systemEventChipAnimateOut?.invoke(systemInfoView) 312 } 313 else -> { 314 // Nothing to do here 315 } 316 } 317 } else { 318 systemInfoView.adjustVisibility(baseVis) 319 } 320 } 321 } 322 } 323 } 324 } 325 } 326 327 /** Bind the (legacy) single primary ongoing activity chip with the status bar visibility */ bindLegacyPrimaryOngoingActivityChipWithVisibilitynull328 private fun bindLegacyPrimaryOngoingActivityChipWithVisibility( 329 areChipsAllowed: Boolean, 330 primaryChipModel: OngoingActivityChipModel, 331 primaryChipViewBinding: OngoingActivityChipViewBinding, 332 ) { 333 if (!areChipsAllowed) { 334 primaryChipViewBinding.rootView.hide(shouldAnimateChange = false) 335 } else { 336 when (primaryChipModel) { 337 is OngoingActivityChipModel.Active -> { 338 primaryChipViewBinding.rootView.show(shouldAnimateChange = true) 339 } 340 341 is OngoingActivityChipModel.Inactive -> { 342 primaryChipViewBinding.rootView.hide( 343 state = View.GONE, 344 shouldAnimateChange = primaryChipModel.shouldAnimate, 345 ) 346 } 347 } 348 } 349 } 350 351 /** Bind the primary/secondary chips along with the home status bar's visibility */ bindOngoingActivityChipsWithVisibilitynull352 private fun bindOngoingActivityChipsWithVisibility( 353 areChipsAllowed: Boolean, 354 chips: MultipleOngoingActivityChipsModelLegacy, 355 primaryChipViewBinding: OngoingActivityChipViewBinding, 356 secondaryChipViewBinding: OngoingActivityChipViewBinding, 357 ) { 358 if (!areChipsAllowed) { 359 primaryChipViewBinding.rootView.hide(shouldAnimateChange = false) 360 secondaryChipViewBinding.rootView.hide(shouldAnimateChange = false) 361 } else { 362 primaryChipViewBinding.rootView.adjustVisibility(chips.primary.toVisibilityModel()) 363 secondaryChipViewBinding.rootView.adjustVisibility(chips.secondary.toVisibilityModel()) 364 } 365 } 366 isAnimatingChipnull367 private fun SystemEventAnimationState.isAnimatingChip() = 368 when (this) { 369 AnimatingIn, 370 AnimatingOut, 371 RunningChipAnim -> true 372 else -> false 373 } 374 OngoingActivityChipModelnull375 private fun OngoingActivityChipModel.toVisibilityModel(): VisibilityModel { 376 return VisibilityModel( 377 visibility = if (this is OngoingActivityChipModel.Active) View.VISIBLE else View.GONE, 378 // TODO(b/364653005): Figure out the animation story here. 379 shouldAnimateChange = true, 380 ) 381 } 382 animateLightsOutViewnull383 private fun animateLightsOutView(view: View, visible: Boolean) { 384 view.animate().cancel() 385 386 val alpha = if (visible) 1f else 0f 387 val duration = if (visible) 750L else 250L 388 val visibility = if (visible) View.VISIBLE else View.GONE 389 390 if (visible) { 391 view.alpha = 0f 392 view.visibility = View.VISIBLE 393 } 394 395 view 396 .animate() 397 .alpha(alpha) 398 .setDuration(duration) 399 .setListener( 400 object : AnimatorListenerAdapter() { 401 override fun onAnimationEnd(animation: Animator) { 402 view.alpha = alpha 403 view.visibility = visibility 404 // Unset the listener, otherwise this may persist for 405 // another view property animation 406 view.animate().setListener(null) 407 } 408 } 409 ) 410 .start() 411 } 412 adjustVisibilitynull413 private fun View.adjustVisibility(model: VisibilityModel) { 414 if (model.visibility == View.VISIBLE) { 415 this.show(model.shouldAnimateChange) 416 } else { 417 this.hide(model.visibility, model.shouldAnimateChange) 418 } 419 } 420 421 /** 422 * Hide the view for initialization, but skip if it's already hidden and does not cancel 423 * animations. 424 */ Viewnull425 private fun View.hideInitially(state: Int = View.INVISIBLE) { 426 if (visibility == View.INVISIBLE || visibility == View.GONE) { 427 return 428 } 429 alpha = 0f 430 visibility = state 431 } 432 433 // See CollapsedStatusBarFragment#hide. hidenull434 private fun View.hide(state: Int = View.INVISIBLE, shouldAnimateChange: Boolean) { 435 animate().cancel() 436 437 if ( 438 (visibility == View.INVISIBLE && state == View.INVISIBLE) || 439 (visibility == View.GONE && state == View.GONE) 440 ) { 441 return 442 } 443 val isAlreadyHidden = visibility == View.INVISIBLE || visibility == View.GONE 444 if (!shouldAnimateChange || isAlreadyHidden) { 445 alpha = 0f 446 visibility = state 447 return 448 } 449 450 animate() 451 .alpha(0f) 452 .setDuration(CollapsedStatusBarFragment.FADE_OUT_DURATION.toLong()) 453 .setStartDelay(0) 454 .setInterpolator(Interpolators.ALPHA_OUT) 455 .withEndAction { visibility = state } 456 } 457 458 // See CollapsedStatusBarFragment#show. shownull459 private fun View.show(shouldAnimateChange: Boolean) { 460 animate().cancel() 461 if (visibility == View.VISIBLE && alpha >= 1f) { 462 return 463 } 464 visibility = View.VISIBLE 465 if (!shouldAnimateChange) { 466 alpha = 1f 467 return 468 } 469 animate() 470 .alpha(1f) 471 .setDuration(CollapsedStatusBarFragment.FADE_IN_DURATION.toLong()) 472 .setInterpolator(Interpolators.ALPHA_IN) 473 .setStartDelay(CollapsedStatusBarFragment.FADE_IN_DELAY.toLong()) 474 // We need to clean up any pending end action from animateHide if we call both hide and 475 // show in the same frame before the animation actually gets started. 476 // cancel() doesn't really remove the end action. 477 .withEndAction(null) 478 479 // TODO(b/364360986): Synchronize the motion with the Keyguard fading if necessary. 480 } 481 } 482 483 /** Listener for various events that may affect the status bar's visibility. */ 484 interface StatusBarVisibilityChangeListener { 485 /** 486 * Called when the status bar visibility might have changed due to the device moving to a 487 * different state. 488 */ onStatusBarVisibilityMaybeChangednull489 fun onStatusBarVisibilityMaybeChanged() 490 491 /** Called when a transition from lockscreen to dream has started. */ 492 fun onTransitionFromLockscreenToDreamStarted() 493 494 /** 495 * Called when the status of the ongoing activity chip (active or not active) has changed. 496 * 497 * @param shouldAnimate true if the chip should animate in/out, and false if the chip should 498 * immediately appear/disappear. 499 */ 500 fun onOngoingActivityStatusChanged( 501 hasPrimaryOngoingActivity: Boolean, 502 hasSecondaryOngoingActivity: Boolean, 503 shouldAnimate: Boolean, 504 ) 505 506 /** 507 * Called when the scene state has changed such that the home status bar is newly allowed or no 508 * longer allowed. See [HomeStatusBarViewModel.isHomeStatusBarAllowed]. 509 */ 510 fun onIsHomeStatusBarAllowedBySceneChanged(isHomeStatusBarAllowedByScene: Boolean) 511 } 512