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