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