1 /*
2  *  Copyright (C) 2023 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 
18 package com.android.systemui.keyguard.domain.interactor
19 
20 import android.content.Context
21 import androidx.annotation.DimenRes
22 import com.android.systemui.R
23 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
24 import com.android.systemui.dagger.SysUISingleton
25 import com.android.systemui.dagger.qualifiers.Application
26 import com.android.systemui.doze.util.BurnInHelperWrapper
27 import javax.inject.Inject
28 import kotlinx.coroutines.CoroutineScope
29 import kotlinx.coroutines.ExperimentalCoroutinesApi
30 import kotlinx.coroutines.flow.SharingStarted
31 import kotlinx.coroutines.flow.StateFlow
32 import kotlinx.coroutines.flow.flatMapLatest
33 import kotlinx.coroutines.flow.mapLatest
34 import kotlinx.coroutines.flow.stateIn
35 
36 /** Encapsulates business-logic related to Ambient Display burn-in offsets. */
37 @ExperimentalCoroutinesApi
38 @SysUISingleton
39 class BurnInInteractor
40 @Inject
41 constructor(
42     private val context: Context,
43     private val burnInHelperWrapper: BurnInHelperWrapper,
44     @Application private val scope: CoroutineScope,
45     private val configurationRepository: ConfigurationRepository,
46     private val keyguardInteractor: KeyguardInteractor,
47 ) {
48     val udfpsBurnInXOffset: StateFlow<Int> =
49         burnInOffsetDefinedInPixels(R.dimen.udfps_burn_in_offset_x, isXAxis = true)
50     val udfpsBurnInYOffset: StateFlow<Int> =
51         burnInOffsetDefinedInPixels(R.dimen.udfps_burn_in_offset_y, isXAxis = false)
52     val udfpsBurnInProgress: StateFlow<Float> =
53         keyguardInteractor.dozeTimeTick
54             .mapLatest { burnInHelperWrapper.burnInProgressOffset() }
55             .stateIn(scope, SharingStarted.Lazily, burnInHelperWrapper.burnInProgressOffset())
56 
57     /**
58      * Use for max burn-in offsets that are NOT specified in pixels. This flow will recalculate the
59      * max burn-in offset on any configuration changes. If the max burn-in offset is specified in
60      * pixels, use [burnInOffsetDefinedInPixels].
61      */
62     private fun burnInOffset(
63         @DimenRes maxBurnInOffsetResourceId: Int,
64         isXAxis: Boolean,
65     ): StateFlow<Int> {
66         return configurationRepository.onAnyConfigurationChange
67             .flatMapLatest {
68                 val maxBurnInOffsetPixels =
69                     context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId)
70                 keyguardInteractor.dozeTimeTick.mapLatest {
71                     calculateOffset(maxBurnInOffsetPixels, isXAxis)
72                 }
73             }
74             .stateIn(
75                 scope,
76                 SharingStarted.Lazily,
77                 calculateOffset(
78                     context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId),
79                     isXAxis,
80                 )
81             )
82     }
83 
84     /**
85      * Use for max burn-in offBurn-in offsets that ARE specified in pixels. This flow will apply the
86      * a scale for any resolution changes. If the max burn-in offset is specified in dp, use
87      * [burnInOffset].
88      */
89     private fun burnInOffsetDefinedInPixels(
90         @DimenRes maxBurnInOffsetResourceId: Int,
91         isXAxis: Boolean,
92     ): StateFlow<Int> {
93         return configurationRepository.scaleForResolution
94             .flatMapLatest { scale ->
95                 val maxBurnInOffsetPixels =
96                     context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId)
97                 keyguardInteractor.dozeTimeTick.mapLatest {
98                     calculateOffset(maxBurnInOffsetPixels, isXAxis, scale)
99                 }
100             }
101             .stateIn(
102                 scope,
103                 SharingStarted.WhileSubscribed(),
104                 calculateOffset(
105                     context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId),
106                     isXAxis,
107                     configurationRepository.getResolutionScale(),
108                 )
109             )
110     }
111 
112     private fun calculateOffset(
113         maxBurnInOffsetPixels: Int,
114         isXAxis: Boolean,
115         scale: Float = 1f
116     ): Int {
117         return (burnInHelperWrapper.burnInOffset(maxBurnInOffsetPixels, isXAxis) * scale).toInt()
118     }
119 }
120