• 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.ui.viewmodel
18 
19 import android.content.Context
20 import android.content.res.Configuration
21 import android.graphics.Rect
22 import android.graphics.Region
23 import android.view.View
24 import android.view.ViewTreeObserver.InternalInsetsInfo
25 import com.android.systemui.res.R
26 import com.android.systemui.statusbar.policy.ConfigurationController
27 import com.android.systemui.statusbar.policy.DevicePostureController
28 import com.android.systemui.statusbar.policy.devicePosture
29 import com.android.systemui.statusbar.policy.onConfigChanged
30 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
31 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
32 import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor
33 import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
34 import com.android.systemui.volume.dialog.shared.model.VolumeDialogStateModel
35 import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
36 import com.android.systemui.volume.dialog.shared.model.streamLabel
37 import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSlidersInteractor
38 import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
39 import javax.inject.Inject
40 import kotlinx.coroutines.CoroutineScope
41 import kotlinx.coroutines.flow.Flow
42 import kotlinx.coroutines.flow.SharingStarted
43 import kotlinx.coroutines.flow.StateFlow
44 import kotlinx.coroutines.flow.combine
45 import kotlinx.coroutines.flow.map
46 import kotlinx.coroutines.flow.onStart
47 import kotlinx.coroutines.flow.stateIn
48 import kotlinx.coroutines.suspendCancellableCoroutine
49 
50 /** Provides a state for the Volume Dialog. */
51 @VolumeDialogScope
52 class VolumeDialogViewModel
53 @Inject
54 constructor(
55     private val context: Context,
56     @VolumeDialog coroutineScope: CoroutineScope,
57     private val dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
58     volumeDialogSlidersInteractor: VolumeDialogSlidersInteractor,
59     private val volumeDialogStateInteractor: VolumeDialogStateInteractor,
60     devicePostureController: DevicePostureController,
61     configurationController: ConfigurationController,
62 ) {
63 
64     val isHalfOpened: Flow<Boolean> =
65         combine(
66             devicePostureController.devicePosture(),
67             configurationController.onConfigChanged.onStart {
68                 emit(context.resources.configuration)
69             },
70         ) { devicePosture, configuration ->
71             shouldOffsetVolumeDialog(devicePosture, configuration)
72         }
73     val dialogVisibilityModel: Flow<VolumeDialogVisibilityModel> =
74         dialogVisibilityInteractor.dialogVisibility
75     val dialogTitle: StateFlow<String> =
76         combine(
77                 volumeDialogStateInteractor.volumeDialogState,
78                 volumeDialogSlidersInteractor.sliders.map { it.slider },
79             ) { state: VolumeDialogStateModel, sliderType: VolumeDialogSliderType ->
80                 state.streamModels[sliderType.audioStream]?.let { model ->
81                     context.getString(R.string.volume_dialog_title, model.streamLabel(context))
82                 } ?: ""
83             }
84             .stateIn(coroutineScope, SharingStarted.Eagerly, "")
85 
86     private val touchableBoundsViews: MutableCollection<View> = mutableSetOf()
87 
88     /** @return true when the foldable device screen curve is in the way of the volume dialog */
89     private fun shouldOffsetVolumeDialog(devicePosture: Int, config: Configuration): Boolean {
90         val isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE
91         val isHalfOpen = devicePosture == DevicePostureController.DEVICE_POSTURE_HALF_OPENED
92         return isLandscape && isHalfOpen
93     }
94 
95     fun fillTouchableBounds(internalInsetsInfo: InternalInsetsInfo) {
96         for (child in touchableBoundsViews) {
97             val boundsRect = Rect()
98             internalInsetsInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION)
99 
100             child.getBoundsInWindow(boundsRect, false)
101             internalInsetsInfo.touchableRegion.op(boundsRect, Region.Op.UNION)
102         }
103     }
104 
105     suspend fun addTouchableBounds(vararg views: View): Nothing = suspendCancellableCoroutine {
106         touchableBoundsViews.addAll(views)
107         it.invokeOnCancellation { touchableBoundsViews.removeAll(views.toSet()) }
108     }
109 
110     fun onHover(isHovering: Boolean) {
111         volumeDialogStateInteractor.setHovering(isHovering)
112         resetDialogTimeout()
113     }
114 
115     fun resetDialogTimeout() {
116         dialogVisibilityInteractor.resetDismissTimeout()
117     }
118 }
119