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