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 package com.android.systemui.biometrics.data.repository
18 
19 import android.content.Context
20 import android.util.DisplayMetrics
21 import android.util.Size
22 import android.view.DisplayInfo
23 import com.android.systemui.biometrics.shared.model.DisplayRotation
24 import com.android.systemui.biometrics.shared.model.toDisplayRotation
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.dagger.qualifiers.Application
27 import com.android.systemui.dagger.qualifiers.Background
28 import com.android.systemui.display.data.repository.DeviceStateRepository
29 import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState.REAR_DISPLAY
30 import com.android.systemui.display.data.repository.DisplayRepository
31 import javax.inject.Inject
32 import kotlin.math.min
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.flow.SharingStarted
35 import kotlinx.coroutines.flow.StateFlow
36 import kotlinx.coroutines.flow.map
37 import kotlinx.coroutines.flow.stateIn
38 
39 /** Repository for the current state of the display */
40 interface DisplayStateRepository {
41     /**
42      * If true, the direction rotation is applied to get to an application's requested orientation
43      * is reversed. Normally, the model is that landscape is clockwise from portrait; thus on a
44      * portrait device an app requesting landscape will cause a clockwise rotation, and on a
45      * landscape device an app requesting portrait will cause a counter-clockwise rotation. Setting
46      * true here reverses that logic. See go/natural-orientation for context.
47      */
48     val isReverseDefaultRotation: Boolean
49 
50     /** Provides the current rear display state. */
51     val isInRearDisplayMode: StateFlow<Boolean>
52 
53     /** Provides the current display rotation */
54     val currentRotation: StateFlow<DisplayRotation>
55 
56     /** Provides the current display size */
57     val currentDisplaySize: StateFlow<Size>
58 
59     /** Provides whether the current display is large screen */
60     val isLargeScreen: StateFlow<Boolean>
61 }
62 
63 @SysUISingleton
64 class DisplayStateRepositoryImpl
65 @Inject
66 constructor(
67     @Background backgroundScope: CoroutineScope,
68     @Application val context: Context,
69     deviceStateRepository: DeviceStateRepository,
70     displayRepository: DisplayRepository,
71 ) : DisplayStateRepository {
72     override val isReverseDefaultRotation =
73         context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
74 
75     override val isInRearDisplayMode: StateFlow<Boolean> =
76         deviceStateRepository.state
<lambda>null77             .map { it == REAR_DISPLAY }
78             .stateIn(
79                 backgroundScope,
80                 started = SharingStarted.Eagerly,
81                 initialValue = false,
82             )
83 
getDisplayInfonull84     private fun getDisplayInfo(): DisplayInfo {
85         val cachedDisplayInfo = DisplayInfo()
86         context.display?.getDisplayInfo(cachedDisplayInfo)
87         return cachedDisplayInfo
88     }
89 
90     private val currentDisplayInfo: StateFlow<DisplayInfo> =
91         displayRepository.displayChangeEvent
<lambda>null92             .map { getDisplayInfo() }
93             .stateIn(
94                 backgroundScope,
95                 started = SharingStarted.Eagerly,
96                 initialValue = getDisplayInfo(),
97             )
98 
rotationToDisplayRotationnull99     private fun rotationToDisplayRotation(rotation: Int): DisplayRotation {
100         var adjustedRotation = rotation
101         if (isReverseDefaultRotation) {
102             adjustedRotation = (rotation + 1) % 4
103         }
104         return adjustedRotation.toDisplayRotation()
105     }
106 
107     override val currentRotation: StateFlow<DisplayRotation> =
108         currentDisplayInfo
<lambda>null109             .map { rotationToDisplayRotation(it.rotation) }
110             .stateIn(
111                 backgroundScope,
112                 started = SharingStarted.WhileSubscribed(),
113                 initialValue = rotationToDisplayRotation(currentDisplayInfo.value.rotation)
114             )
115 
116     override val currentDisplaySize: StateFlow<Size> =
117         currentDisplayInfo
<lambda>null118             .map { Size(it.naturalWidth, it.naturalHeight) }
119             .stateIn(
120                 backgroundScope,
121                 started = SharingStarted.WhileSubscribed(),
122                 initialValue =
123                     Size(
124                         currentDisplayInfo.value.naturalWidth,
125                         currentDisplayInfo.value.naturalHeight
126                     ),
127             )
128 
129     override val isLargeScreen: StateFlow<Boolean> =
130         currentDisplayInfo
<lambda>null131             .map {
132                 // copied from systemui/shared/...Utilities.java
133                 val smallestWidth =
134                     dpiFromPx(
135                         min(it.logicalWidth, it.logicalHeight).toFloat(),
136                         context.resources.configuration.densityDpi
137                     )
138                 smallestWidth >= LARGE_SCREEN_MIN_DPS
139             }
140             .stateIn(
141                 backgroundScope,
142                 started = SharingStarted.Eagerly,
143                 initialValue = false,
144             )
145 
dpiFromPxnull146     private fun dpiFromPx(size: Float, densityDpi: Int): Float {
147         val densityRatio = densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
148         return size / densityRatio
149     }
150 
151     companion object {
152         const val TAG = "DisplayStateRepositoryImpl"
153         const val LARGE_SCREEN_MIN_DPS = 600f
154     }
155 }
156