1 /*
<lambda>null2 * Copyright (C) 2025 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.shade.domain.interactor
18
19 import android.util.Log
20 import com.android.systemui.dagger.SysUISingleton
21 import com.android.systemui.dagger.qualifiers.Background
22 import com.android.systemui.scene.shared.flag.SceneContainerFlag
23 import com.android.systemui.shade.ShadeTraceLogger.traceWaitForExpansion
24 import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement
25 import com.android.systemui.util.kotlin.Utils.Companion.combineState
26 import javax.inject.Inject
27 import kotlin.coroutines.CoroutineContext
28 import kotlin.time.Duration
29 import kotlin.time.Duration.Companion.seconds
30 import kotlinx.coroutines.CoroutineScope
31 import kotlinx.coroutines.flow.MutableStateFlow
32 import kotlinx.coroutines.flow.SharingStarted
33 import kotlinx.coroutines.flow.StateFlow
34 import kotlinx.coroutines.flow.first
35 import kotlinx.coroutines.withContext
36 import kotlinx.coroutines.withTimeoutOrNull
37
38 /**
39 * Wrapper around [ShadeInteractor] to facilitate expansion and collapse of Notifications and quick
40 * settings.
41 *
42 * Specifically created to simplify [ShadeDisplaysInteractor] logic.
43 *
44 * NOTE: with [SceneContainerFlag] disabled, [currentlyExpandedElement] will always return `null`!
45 */
46 interface ShadeExpandedStateInteractor {
47 /** Returns the expanded [ShadeElement]. If none is, returns null. */
48 val currentlyExpandedElement: StateFlow<ShadeElement?>
49
50 /** An element from the shade window that can be expanded or collapsed. */
51 sealed class ShadeElement {
52 /** Expands the shade element, returning when the expansion is done */
53 abstract suspend fun expand(reason: String)
54
55 /** Collapses the shade element, returning when the collapse is done. */
56 abstract suspend fun collapse(reason: String)
57 }
58 }
59
60 private val EXPAND_COLLAPSE_TIMEOUT: Duration = 1.seconds
61
62 @SysUISingleton
63 class ShadeExpandedStateInteractorImpl
64 @Inject
65 constructor(
66 shadeInteractor: ShadeInteractor,
67 @Background private val bgScope: CoroutineScope,
68 private val notificationElement: NotificationShadeElement,
69 private val qsElement: QSShadeElement,
70 ) : ShadeExpandedStateInteractor {
71
72 override val currentlyExpandedElement: StateFlow<ShadeElement?> =
73 if (SceneContainerFlag.isEnabled) {
74 combineState(
75 shadeInteractor.isShadeAnyExpanded,
76 shadeInteractor.isQsExpanded,
77 bgScope,
78 SharingStarted.Eagerly,
isQsExpandednull79 ) { isShadeAnyExpanded, isQsExpanded ->
80 when {
81 isShadeAnyExpanded -> notificationElement
82 isQsExpanded -> qsElement
83 else -> null
84 }
85 }
86 } else {
87 MutableStateFlow(null)
88 }
89 }
90
waitUntilnull91 private suspend fun StateFlow<Float>.waitUntil(f: Float, coroutineContext: CoroutineContext) {
92 // it's important to not do this in the main thread otherwise it will block any rendering.
93 withContext(coroutineContext) {
94 withTimeoutOrNull(EXPAND_COLLAPSE_TIMEOUT) {
95 traceWaitForExpansion(expansion = f) { first { it == f } }
96 }
97 ?: Log.e(
98 "ShadeExpStateInteractor",
99 "Timed out after ${EXPAND_COLLAPSE_TIMEOUT.inWholeMilliseconds}ms while waiting " +
100 "for expansion to match $f. Current one: $value",
101 )
102 }
103 }
104
105 @SysUISingleton
106 class NotificationShadeElement
107 @Inject
108 constructor(
109 private val shadeInteractor: ShadeInteractor,
110 @Background private val bgContext: CoroutineContext,
111 ) : ShadeElement() {
expandnull112 override suspend fun expand(reason: String) {
113 if (SceneContainerFlag.isEnabled) {
114 shadeInteractor.expandNotificationsShade(reason)
115 shadeInteractor.shadeExpansion.waitUntil(1f, bgContext)
116 }
117 }
118
collapsenull119 override suspend fun collapse(reason: String) {
120 if (SceneContainerFlag.isEnabled) {
121 shadeInteractor.collapseNotificationsShade(reason)
122 shadeInteractor.shadeExpansion.waitUntil(0f, bgContext)
123 }
124 }
125 }
126
127 @SysUISingleton
128 class QSShadeElement
129 @Inject
130 constructor(
131 private val shadeInteractor: ShadeInteractor,
132 @Background private val bgContext: CoroutineContext,
133 ) : ShadeElement() {
expandnull134 override suspend fun expand(reason: String) {
135 if (SceneContainerFlag.isEnabled) {
136 shadeInteractor.expandQuickSettingsShade(reason)
137 shadeInteractor.qsExpansion.waitUntil(1f, bgContext)
138 }
139 }
140
collapsenull141 override suspend fun collapse(reason: String) {
142 if (SceneContainerFlag.isEnabled) {
143 shadeInteractor.collapseQuickSettingsShade(reason)
144 shadeInteractor.qsExpansion.waitUntil(0f, bgContext)
145 }
146 }
147 }
148