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.keyguard.ui.binder
18 
19 import android.util.TypedValue
20 import android.view.View
21 import android.view.ViewGroup
22 import android.widget.TextView
23 import androidx.lifecycle.Lifecycle
24 import androidx.lifecycle.repeatOnLifecycle
25 import com.android.systemui.R
26 import com.android.systemui.flags.FeatureFlags
27 import com.android.systemui.flags.Flags
28 import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
29 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
30 import com.android.systemui.lifecycle.repeatWhenAttached
31 import com.android.systemui.statusbar.KeyguardIndicationController
32 import kotlinx.coroutines.DisposableHandle
33 import kotlinx.coroutines.ExperimentalCoroutinesApi
34 import kotlinx.coroutines.flow.MutableStateFlow
35 import kotlinx.coroutines.flow.combine
36 import kotlinx.coroutines.flow.flatMapLatest
37 import kotlinx.coroutines.flow.map
38 import kotlinx.coroutines.launch
39 
40 /**
41  * Binds a keyguard indication area view to its view-model.
42  *
43  * To use this properly, users should maintain a one-to-one relationship between the [View] and the
44  * view-binding, binding each view only once. It is okay and expected for the same instance of the
45  * view-model to be reused for multiple view/view-binder bindings.
46  */
47 @OptIn(ExperimentalCoroutinesApi::class)
48 object KeyguardIndicationAreaBinder {
49 
50     /** Binds the view to the view-model, continuing to update the former based on the latter. */
51     @JvmStatic
52     fun bind(
53         view: ViewGroup,
54         viewModel: KeyguardIndicationAreaViewModel,
55         keyguardRootViewModel: KeyguardRootViewModel,
56         indicationController: KeyguardIndicationController,
57         featureFlags: FeatureFlags,
58     ): DisposableHandle {
59         val indicationArea: ViewGroup = view.requireViewById(R.id.keyguard_indication_area)
60         indicationController.setIndicationArea(indicationArea)
61 
62         val indicationText: TextView = indicationArea.requireViewById(R.id.keyguard_indication_text)
63         val indicationTextBottom: TextView =
64             indicationArea.requireViewById(R.id.keyguard_indication_text_bottom)
65 
66         view.clipChildren = false
67         view.clipToPadding = false
68 
69         val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
70         val disposableHandle =
71             view.repeatWhenAttached {
72                 repeatOnLifecycle(Lifecycle.State.STARTED) {
73                     launch {
74                         if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
75                             keyguardRootViewModel.alpha.collect { alpha ->
76                                 indicationArea.apply {
77                                     this.importantForAccessibility =
78                                         if (alpha == 0f) {
79                                             View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
80                                         } else {
81                                             View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
82                                         }
83                                     this.alpha = alpha
84                                 }
85                             }
86                         } else {
87                             viewModel.alpha.collect { alpha ->
88                                 indicationArea.apply {
89                                     this.importantForAccessibility =
90                                         if (alpha == 0f) {
91                                             View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
92                                         } else {
93                                             View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
94                                         }
95                                     this.alpha = alpha
96                                 }
97                             }
98                         }
99                     }
100 
101                     launch {
102                         viewModel.indicationAreaTranslationX.collect { translationX ->
103                             indicationArea.translationX = translationX
104                         }
105                     }
106 
107                     launch {
108                         combine(
109                                 viewModel.isIndicationAreaPadded,
110                                 configurationBasedDimensions.map { it.indicationAreaPaddingPx },
111                             ) { isPadded, paddingIfPaddedPx ->
112                                 if (isPadded) {
113                                     paddingIfPaddedPx
114                                 } else {
115                                     0
116                                 }
117                             }
118                             .collect { paddingPx ->
119                                 indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
120                             }
121                     }
122 
123                     launch {
124                         configurationBasedDimensions
125                             .map { it.defaultBurnInPreventionYOffsetPx }
126                             .flatMapLatest { defaultBurnInOffsetY ->
127                                 viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
128                             }
129                             .collect { translationY -> indicationArea.translationY = translationY }
130                     }
131 
132                     launch {
133                         configurationBasedDimensions.collect { dimensions ->
134                             indicationText.setTextSize(
135                                 TypedValue.COMPLEX_UNIT_PX,
136                                 dimensions.indicationTextSizePx.toFloat(),
137                             )
138                             indicationTextBottom.setTextSize(
139                                 TypedValue.COMPLEX_UNIT_PX,
140                                 dimensions.indicationTextSizePx.toFloat(),
141                             )
142                         }
143                     }
144 
145                     launch {
146                         viewModel.configurationChange.collect {
147                             configurationBasedDimensions.value = loadFromResources(view)
148                         }
149                     }
150                 }
151             }
152         return disposableHandle
153     }
154 
155     private fun loadFromResources(view: View): ConfigurationBasedDimensions {
156         return ConfigurationBasedDimensions(
157             defaultBurnInPreventionYOffsetPx =
158                 view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset),
159             indicationAreaPaddingPx =
160                 view.resources.getDimensionPixelOffset(R.dimen.keyguard_indication_area_padding),
161             indicationTextSizePx =
162                 view.resources.getDimensionPixelSize(
163                     com.android.internal.R.dimen.text_size_small_material,
164                 ),
165         )
166     }
167 
168     private data class ConfigurationBasedDimensions(
169         val defaultBurnInPreventionYOffsetPx: Int,
170         val indicationAreaPaddingPx: Int,
171         val indicationTextSizePx: Int,
172     )
173 }
174