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.qs.ui.adapter 18 19 import android.content.Context 20 import android.content.pm.ActivityInfo 21 import android.os.Bundle 22 import android.view.View 23 import androidx.annotation.VisibleForTesting 24 import androidx.asynclayoutinflater.view.AsyncLayoutInflater 25 import com.android.app.tracing.coroutines.launchTraced as launch 26 import com.android.settingslib.applications.InterestingConfigChanges 27 import com.android.systemui.Dumpable 28 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor 29 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.dagger.qualifiers.Application 32 import com.android.systemui.dagger.qualifiers.Main 33 import com.android.systemui.dump.DumpManager 34 import com.android.systemui.plugins.qs.QSContainerController 35 import com.android.systemui.qs.QSContainerImpl 36 import com.android.systemui.qs.QSImpl 37 import com.android.systemui.qs.dagger.QSSceneComponent 38 import com.android.systemui.res.R 39 import com.android.systemui.settings.brightness.MirrorController 40 import com.android.systemui.shade.ShadeDisplayAware 41 import com.android.systemui.shade.domain.interactor.ShadeInteractor 42 import com.android.systemui.shade.shared.model.ShadeMode 43 import com.android.systemui.util.kotlin.sample 44 import java.io.PrintWriter 45 import javax.inject.Inject 46 import javax.inject.Provider 47 import kotlin.coroutines.resume 48 import kotlin.coroutines.suspendCoroutine 49 import kotlinx.coroutines.CoroutineDispatcher 50 import kotlinx.coroutines.CoroutineScope 51 import kotlinx.coroutines.channels.BufferOverflow 52 import kotlinx.coroutines.flow.MutableSharedFlow 53 import kotlinx.coroutines.flow.MutableStateFlow 54 import kotlinx.coroutines.flow.SharingStarted 55 import kotlinx.coroutines.flow.StateFlow 56 import kotlinx.coroutines.flow.asStateFlow 57 import kotlinx.coroutines.flow.combine 58 import kotlinx.coroutines.flow.filterNotNull 59 import kotlinx.coroutines.flow.map 60 import kotlinx.coroutines.flow.stateIn 61 import kotlinx.coroutines.flow.update 62 import kotlinx.coroutines.withContext 63 64 // TODO(307945185) Split View concerns into a ViewBinder 65 /** Adapter to use between Scene system and [QSImpl] */ 66 interface QSSceneAdapter { 67 68 /** 69 * Whether we are currently customizing or entering the customizer. 70 * 71 * @see CustomizerState.isCustomizing 72 */ 73 val isCustomizing: StateFlow<Boolean> 74 75 /** 76 * Whether the customizer is showing. This includes animating into and out of it. 77 * 78 * @see CustomizerState.isShowing 79 */ 80 val isCustomizerShowing: StateFlow<Boolean> 81 82 /** 83 * The duration of the current animation in/out of customizer. If not in an animating state, 84 * this duration is 0 (to match show/hide immediately). 85 * 86 * @see CustomizerState.Animating.animationDuration 87 */ 88 val customizerAnimationDuration: StateFlow<Int> 89 90 /** 91 * A view with the QS content ([QSContainerImpl]), managed by an instance of [QSImpl] tracked by 92 * the interactor. 93 * 94 * A null value means that there is no inflated view yet. See [inflate]. 95 */ 96 val qsView: StateFlow<View?> 97 98 /** Sets the [MirrorController] in [QSImpl]. Set to `null` to remove. */ 99 fun setBrightnessMirrorController(mirrorController: MirrorController?) 100 101 /** 102 * Inflate an instance of [QSImpl] for this context. Once inflated, it will be available in 103 * [qsView]. Re-inflations due to configuration changes will use the last used [context]. 104 */ 105 suspend fun inflate(context: Context) 106 107 /** 108 * Set the current state for QS. [state]. 109 * 110 * This will not trigger expansion (animation between QQS or QS) or squishiness to be applied. 111 * For that, use [applyLatestExpansionAndSquishiness] outside of the composition phase. 112 */ 113 fun setState(state: State) 114 115 /** 116 * Explicitly applies the expansion and squishiness value from the latest state set. Call this 117 * only outside of the composition phase as this will call [QSImpl.setQsExpansion] that is 118 * normally called during animations. In particular, this will read the value of 119 * [State.squishiness], that is not safe to read in the composition phase. 120 */ 121 fun applyLatestExpansionAndSquishiness() 122 123 /** Propagates the bottom nav bar size to [QSImpl] to be used as necessary. */ 124 suspend fun applyBottomNavBarPadding(padding: Int) 125 126 /** The current height of QQS in the current [qsView], or 0 if there's no view. */ 127 val qqsHeight: Int 128 129 /** @return height with the squishiness fraction applied. */ 130 val squishedQqsHeight: Int 131 132 /** 133 * The current height of QS in the current [qsView], or 0 if there's no view. If customizing, it 134 * will return the height allocated to the customizer. 135 */ 136 val qsHeight: Int 137 138 /** @return height with the squishiness fraction applied. */ 139 val squishedQsHeight: Int 140 141 /** Compatibility for use by LockscreenShadeTransitionController. Matches default from [QS] */ 142 val isQsFullyCollapsed: Boolean 143 get() = true 144 145 /** Request that the customizer be closed. Possibly animating it. */ 146 fun requestCloseCustomizer() 147 148 sealed interface State { 149 150 val isVisible: Boolean 151 val expansion: () -> Float 152 val squishiness: () -> Float 153 154 data object CLOSED : State { 155 override val isVisible = false 156 override val expansion = { 0f } 157 override val squishiness = { 1f } 158 } 159 160 /** State for expanding between QQS and QS */ 161 class Expanding(override val expansion: () -> Float) : State { 162 override val isVisible = true 163 override val squishiness = { 1f } 164 } 165 166 /** 167 * State for appearing QQS from Lockscreen or Gone. 168 * 169 * This should not be a data class, as it has a method parameter and even if it's the same 170 * lambda the output value may have changed. 171 */ 172 class UnsquishingQQS(override val squishiness: () -> Float) : State { 173 override val isVisible = true 174 override val expansion = { 0f } 175 } 176 177 /** 178 * State for appearing QS from Lockscreen or Gone, used in Split shade. 179 * 180 * This should not be a data class, as it has a method parameter and even if it's the same 181 * lambda the output value may have changed. 182 */ 183 class UnsquishingQS(override val squishiness: () -> Float) : State { 184 override val isVisible = true 185 override val expansion = { 1f } 186 } 187 188 companion object { 189 // These are special cases of the expansion. 190 val QQS = Expanding { 0f } 191 val QS = Expanding { 1f } 192 193 /** Collapsing from QS to QQS. [progress] is 0f in QS and 1f in QQS. */ 194 fun Collapsing(progress: () -> Float) = Expanding { 1f - progress() } 195 } 196 } 197 } 198 199 @SysUISingleton 200 class QSSceneAdapterImpl 201 @VisibleForTesting 202 constructor( 203 private val qsSceneComponentFactory: QSSceneComponent.Factory, 204 private val qsImplProvider: Provider<QSImpl>, 205 shadeInteractor: ShadeInteractor, 206 displayStateInteractor: DisplayStateInteractor, 207 dumpManager: DumpManager, 208 @Main private val mainDispatcher: CoroutineDispatcher, 209 @Application applicationScope: CoroutineScope, 210 @ShadeDisplayAware private val configurationInteractor: ConfigurationInteractor, 211 private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater, 212 ) : QSContainerController, QSSceneAdapter, Dumpable { 213 214 @Inject 215 constructor( 216 qsSceneComponentFactory: QSSceneComponent.Factory, 217 qsImplProvider: Provider<QSImpl>, 218 shadeInteractor: ShadeInteractor, 219 displayStateInteractor: DisplayStateInteractor, 220 dumpManager: DumpManager, 221 @Main dispatcher: CoroutineDispatcher, 222 @Application scope: CoroutineScope, 223 @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, 224 ) : this( 225 qsSceneComponentFactory, 226 qsImplProvider, 227 shadeInteractor, 228 displayStateInteractor, 229 dumpManager, 230 dispatcher, 231 scope, 232 configurationInteractor, 233 ::AsyncLayoutInflater, 234 ) 235 236 private val bottomNavBarSize = 237 MutableSharedFlow<Int>( 238 extraBufferCapacity = 1, 239 onBufferOverflow = BufferOverflow.DROP_OLDEST, 240 ) 241 private val state = MutableStateFlow<QSSceneAdapter.State>(QSSceneAdapter.State.CLOSED) 242 private val _customizingState: MutableStateFlow<CustomizerState> = 243 MutableStateFlow(CustomizerState.Hidden) 244 val customizerState = _customizingState.asStateFlow() 245 246 override val isCustomizing: StateFlow<Boolean> = 247 customizerState <lambda>null248 .map { it.isCustomizing } 249 .stateIn( 250 applicationScope, 251 SharingStarted.WhileSubscribed(), 252 customizerState.value.isCustomizing, 253 ) 254 override val isCustomizerShowing: StateFlow<Boolean> = 255 customizerState <lambda>null256 .map { it.isShowing } 257 .stateIn( 258 applicationScope, 259 SharingStarted.WhileSubscribed(), 260 customizerState.value.isShowing, 261 ) 262 override val customizerAnimationDuration: StateFlow<Int> = 263 customizerState <lambda>null264 .map { (it as? CustomizerState.Animating)?.animationDuration?.toInt() ?: 0 } 265 .stateIn( 266 applicationScope, 267 SharingStarted.WhileSubscribed(), 268 (customizerState.value as? CustomizerState.Animating)?.animationDuration?.toInt() 269 ?: 0, 270 ) 271 272 private val _qsImpl: MutableStateFlow<QSImpl?> = MutableStateFlow(null) 273 val qsImpl = _qsImpl.asStateFlow() 274 override val qsView: StateFlow<View?> = 275 _qsImpl <lambda>null276 .map { it?.view } 277 .stateIn(applicationScope, SharingStarted.WhileSubscribed(), _qsImpl.value?.view) 278 279 override val qqsHeight: Int 280 get() = qsImpl.value?.qqsHeight ?: 0 281 282 override val squishedQqsHeight: Int 283 get() = qsImpl.value?.squishedQqsHeight ?: 0 284 285 override val qsHeight: Int 286 get() = qsImpl.value?.qsHeight ?: 0 287 288 override val squishedQsHeight: Int 289 get() = qsImpl.value?.squishedQsHeight ?: 0 290 291 // If value is null, there's no QS and therefore it's fully collapsed. 292 override val isQsFullyCollapsed: Boolean 293 get() = qsImpl.value?.isFullyCollapsed ?: true 294 295 // Same config changes as in FragmentHostManager 296 private val interestingChanges = 297 InterestingConfigChanges( 298 ActivityInfo.CONFIG_FONT_SCALE or 299 ActivityInfo.CONFIG_LOCALE or 300 ActivityInfo.CONFIG_ASSETS_PATHS 301 ) 302 303 init { 304 dumpManager.registerDumpable(this) <lambda>null305 applicationScope.launch { 306 launch { 307 state.sample(_customizingState, ::Pair).collect { (state, customizing) -> 308 qsImpl.value?.apply { 309 if (state != QSSceneAdapter.State.QS && customizing.isShowing) { 310 this@apply.closeCustomizerImmediately() 311 } 312 applyState(state) 313 } 314 } 315 } 316 launch { 317 configurationInteractor.configurationValues.collect { config -> 318 if (interestingChanges.applyNewConfig(config)) { 319 // Assumption: The context is always the same and with the same theme. 320 // If colors change they will be reflected as attributes in the theme. 321 qsImpl.value?.view?.let { inflate(it.context) } 322 } else { 323 qsImpl.value?.onConfigurationChanged(config) 324 qsImpl.value?.view?.dispatchConfigurationChanged(config) 325 } 326 } 327 } 328 launch { 329 combine(bottomNavBarSize, qsImpl.filterNotNull(), ::Pair).collect { 330 it.second.applyBottomNavBarToCustomizerPadding(it.first) 331 } 332 } 333 launch { 334 shadeInteractor.shadeMode.collect { 335 qsImpl.value?.setInSplitShade(it == ShadeMode.Split) 336 } 337 } 338 launch { 339 combine(displayStateInteractor.isLargeScreen, qsImpl.filterNotNull(), ::Pair) 340 .collect { it.second.setIsNotificationPanelFullWidth(!it.first) } 341 } 342 } 343 } 344 setCustomizerAnimatingnull345 override fun setCustomizerAnimating(animating: Boolean) { 346 if (_customizingState.value is CustomizerState.Animating && !animating) { 347 _customizingState.update { 348 if (it is CustomizerState.AnimatingIntoCustomizer) { 349 CustomizerState.Showing 350 } else { 351 CustomizerState.Hidden 352 } 353 } 354 } 355 } 356 setCustomizerShowingnull357 override fun setCustomizerShowing(showing: Boolean) { 358 setCustomizerShowing(showing, 0L) 359 } 360 setCustomizerShowingnull361 override fun setCustomizerShowing(showing: Boolean, animationDuration: Long) { 362 _customizingState.update { _ -> 363 if (showing) { 364 if (animationDuration > 0) { 365 CustomizerState.AnimatingIntoCustomizer(animationDuration) 366 } else { 367 CustomizerState.Showing 368 } 369 } else { 370 if (animationDuration > 0) { 371 CustomizerState.AnimatingOutOfCustomizer(animationDuration) 372 } else { 373 CustomizerState.Hidden 374 } 375 } 376 } 377 } 378 setDetailShowingnull379 override fun setDetailShowing(showing: Boolean) {} 380 inflatenull381 override suspend fun inflate(context: Context) { 382 withContext(mainDispatcher) { 383 val inflater = asyncLayoutInflaterFactory(context) 384 val view = suspendCoroutine { continuation -> 385 inflater.inflate(R.layout.qs_panel, null) { view, _, _ -> 386 continuation.resume(view) 387 } 388 } 389 val bundle = Bundle() 390 _qsImpl.value?.onSaveInstanceState(bundle) 391 _qsImpl.value?.onDestroy() 392 val component = qsSceneComponentFactory.create(view) 393 val qs = qsImplProvider.get() 394 qs.onCreate(null) 395 qs.onComponentCreated(component, bundle) 396 _qsImpl.value = qs 397 qs.view.setPadding(0, 0, 0, 0) 398 qs.setContainerController(this@QSSceneAdapterImpl) 399 qs.applyState(state.value) 400 applyLatestExpansionAndSquishiness() 401 } 402 } 403 setStatenull404 override fun setState(state: QSSceneAdapter.State) { 405 this.state.value = state 406 } 407 applyBottomNavBarPaddingnull408 override suspend fun applyBottomNavBarPadding(padding: Int) { 409 bottomNavBarSize.emit(padding) 410 } 411 requestCloseCustomizernull412 override fun requestCloseCustomizer() { 413 qsImpl.value?.closeCustomizer() 414 } 415 setBrightnessMirrorControllernull416 override fun setBrightnessMirrorController(mirrorController: MirrorController?) { 417 qsImpl.value?.setBrightnessMirrorController(mirrorController) 418 } 419 applyStatenull420 private fun QSImpl.applyState(state: QSSceneAdapter.State) { 421 setQsVisible(state.isVisible) 422 setExpanded(state.isVisible && state.expansion() > 0f) 423 setListening(state.isVisible) 424 } 425 applyLatestExpansionAndSquishinessnull426 override fun applyLatestExpansionAndSquishiness() { 427 val qsImpl = _qsImpl.value 428 val state = state.value 429 qsImpl?.setQsExpansion(state.expansion(), 1f, 0f, state.squishiness()) 430 } 431 dumpnull432 override fun dump(pw: PrintWriter, args: Array<out String>) { 433 pw.apply { 434 println("Last state: ${state.value}") 435 println("CustomizerState: ${_customizingState.value}") 436 println("QQS height: $qqsHeight") 437 println("QS height: $qsHeight") 438 } 439 } 440 } 441 442 /** Current state of the customizer */ 443 sealed interface CustomizerState { 444 445 /** 446 * This indicates that some part of the customizer is showing. It could be animating in or out. 447 */ 448 val isShowing: Boolean 449 get() = true 450 451 /** 452 * This indicates that we are currently customizing or animating into it. In particular, when 453 * animating out, this is false. 454 * 455 * @see QSCustomizer.isCustomizing 456 */ 457 val isCustomizing: Boolean 458 get() = false 459 460 sealed interface Animating : CustomizerState { 461 val animationDuration: Long 462 } 463 464 /** Customizer is completely hidden, and not animating */ 465 data object Hidden : CustomizerState { 466 override val isShowing = false 467 } 468 469 /** Customizer is completely showing, and not animating */ 470 data object Showing : CustomizerState { 471 override val isCustomizing = true 472 } 473 474 /** Animating from [Hidden] into [Showing]. */ 475 data class AnimatingIntoCustomizer(override val animationDuration: Long) : Animating { 476 override val isCustomizing = true 477 } 478 479 /** Animating from [Showing] into [Hidden]. */ 480 data class AnimatingOutOfCustomizer(override val animationDuration: Long) : Animating 481 } 482