• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.volume.dialog.domain.interactor
18 
19 import android.annotation.SuppressLint
20 import android.provider.Settings
21 import android.view.accessibility.AccessibilityManager
22 import com.android.systemui.accessibility.data.repository.AccessibilityRepository
23 import com.android.systemui.plugins.VolumeDialogController
24 import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
25 import com.android.systemui.volume.Events
26 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
27 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
28 import com.android.systemui.volume.dialog.data.VolumeDialogVisibilityRepository
29 import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
30 import com.android.systemui.volume.dialog.shared.model.VolumeDialogSafetyWarningModel
31 import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
32 import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Dismissed
33 import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Visible
34 import com.android.systemui.volume.dialog.utils.VolumeTracer
35 import javax.inject.Inject
36 import kotlin.time.Duration
37 import kotlin.time.Duration.Companion.milliseconds
38 import kotlin.time.Duration.Companion.seconds
39 import kotlin.time.DurationUnit
40 import kotlinx.coroutines.CoroutineScope
41 import kotlinx.coroutines.delay
42 import kotlinx.coroutines.flow.Flow
43 import kotlinx.coroutines.flow.MutableSharedFlow
44 import kotlinx.coroutines.flow.SharingStarted
45 import kotlinx.coroutines.flow.first
46 import kotlinx.coroutines.flow.launchIn
47 import kotlinx.coroutines.flow.mapLatest
48 import kotlinx.coroutines.flow.mapNotNull
49 import kotlinx.coroutines.flow.merge
50 import kotlinx.coroutines.flow.onEach
51 import kotlinx.coroutines.flow.stateIn
52 
53 /**
54  * Handles Volume Dialog visibility state. It might change from several sources:
55  * - [com.android.systemui.plugins.VolumeDialogController] requests visibility change;
56  * - it might be dismissed by the inactivity timeout;
57  * - it can be dismissed by the user;
58  */
59 @VolumeDialogPluginScope
60 class VolumeDialogVisibilityInteractor
61 @Inject
62 constructor(
63     @VolumeDialogPlugin coroutineScope: CoroutineScope,
64     callbacksInteractor: VolumeDialogCallbacksInteractor,
65     private val stateInteractor: VolumeDialogStateInteractor,
66     private val tracer: VolumeTracer,
67     private val repository: VolumeDialogVisibilityRepository,
68     private val accessibilityRepository: AccessibilityRepository,
69     private val controller: VolumeDialogController,
70     private val secureSettingsRepository: SecureSettingsRepository,
71 ) {
72 
73     /** @see computeTimeout */
74     private val defaultTimeout = 3.seconds
75 
76     @SuppressLint("SharedFlowCreation")
77     private val mutableDismissDialogEvents = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
78     val dialogVisibility: Flow<VolumeDialogVisibilityModel> =
79         repository.dialogVisibility
80             .onEach { controller.notifyVisible(it is Visible) }
81             .stateIn(coroutineScope, SharingStarted.Eagerly, VolumeDialogVisibilityModel.Invisible)
82 
83     init {
84         merge(
85                 mutableDismissDialogEvents.mapLatest {
86                     delay(computeTimeout())
87                     VolumeDialogEventModel.DismissRequested(Events.DISMISS_REASON_TIMEOUT)
88                 },
89                 callbacksInteractor.event,
90             )
91             .mapNotNull { it.toVisibilityModel() }
92             .onEach { model ->
93                 updateVisibility { model }
94                 if (model is Visible) {
95                     resetDismissTimeout()
96                 }
97             }
98             .launchIn(coroutineScope)
99     }
100 
101     /**
102      * Dismisses the dialog with a given [reason]. The new state will be emitted in the
103      * [dialogVisibility].
104      */
105     fun dismissDialog(reason: Int) {
106         updateVisibility { Dismissed(reason) }
107     }
108 
109     /** Resets current dialog timeout. */
110     fun resetDismissTimeout() {
111         controller.userActivity()
112         mutableDismissDialogEvents.tryEmit(Unit)
113     }
114 
115     private fun updateVisibility(
116         update: (VolumeDialogVisibilityModel) -> VolumeDialogVisibilityModel
117     ) {
118         repository.updateVisibility { currentVisibility ->
119             val newVisibility = update(currentVisibility)
120             // Don't update if the visibility is of the same type
121             if (currentVisibility::class == newVisibility::class) {
122                 currentVisibility
123             } else {
124                 tracer.traceVisibilityStart(newVisibility)
125                 newVisibility
126             }
127         }
128     }
129 
130     private suspend fun computeTimeout(): Duration {
131         val defaultDialogTimeoutMillis =
132             secureSettingsRepository
133                 .getInt(
134                     Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT,
135                     defaultTimeout.toInt(DurationUnit.MILLISECONDS),
136                 )
137                 .milliseconds
138         val currentDialogState = stateInteractor.volumeDialogState.first()
139         return when {
140             currentDialogState.isHovering ->
141                 accessibilityRepository.getRecommendedTimeout(
142                     defaultDialogTimeoutMillis,
143                     AccessibilityManager.FLAG_CONTENT_CONTROLS,
144                 )
145 
146             currentDialogState.isShowingSafetyWarning is VolumeDialogSafetyWarningModel.Visible ->
147                 accessibilityRepository.getRecommendedTimeout(
148                     defaultDialogTimeoutMillis,
149                     AccessibilityManager.FLAG_CONTENT_TEXT or
150                         AccessibilityManager.FLAG_CONTENT_CONTROLS,
151                 )
152 
153             else ->
154                 accessibilityRepository.getRecommendedTimeout(
155                     defaultDialogTimeoutMillis,
156                     AccessibilityManager.FLAG_CONTENT_CONTROLS,
157                 )
158         }
159     }
160 
161     private fun VolumeDialogEventModel.toVisibilityModel(): VolumeDialogVisibilityModel? {
162         return when (this) {
163             is VolumeDialogEventModel.DismissRequested -> Dismissed(reason)
164             is VolumeDialogEventModel.ShowRequested ->
165                 Visible(reason, keyguardLocked, lockTaskModeState)
166 
167             else -> null
168         }
169     }
170 }
171