• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.app.tracing.coroutines.launchTraced as launch
26 import com.android.systemui.Flags
27 import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
28 import com.android.systemui.lifecycle.repeatWhenAttached
29 import com.android.systemui.res.R
30 import com.android.systemui.statusbar.KeyguardIndicationController
31 import com.android.systemui.util.kotlin.DisposableHandles
32 import kotlinx.coroutines.DisposableHandle
33 import kotlinx.coroutines.flow.MutableStateFlow
34 import kotlinx.coroutines.flow.combine
35 import kotlinx.coroutines.flow.flatMapLatest
36 import kotlinx.coroutines.flow.map
37 
38 /**
39  * Binds a keyguard indication area view to its view-model.
40  *
41  * To use this properly, users should maintain a one-to-one relationship between the [View] and the
42  * view-binding, binding each view only once. It is okay and expected for the same instance of the
43  * view-model to be reused for multiple view/view-binder bindings.
44  */
45 object KeyguardIndicationAreaBinder {
46 
47     /** Binds the view to the view-model, continuing to update the former based on the latter. */
48     @JvmStatic
49     fun bind(
50         view: ViewGroup,
51         viewModel: KeyguardIndicationAreaViewModel,
52         indicationController: KeyguardIndicationController,
53     ): DisposableHandle {
54         val disposables = DisposableHandles()
55 
56         // As the indication controller is a singleton, reset the view back to the previous view
57         // once the current view is disposed.
58         val previous = indicationController.indicationArea
59         indicationController.indicationArea = view
60         disposables += DisposableHandle {
61             previous?.let { indicationController.indicationArea = it }
62         }
63 
64         val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text)
65         val indicationTextBottom: TextView =
66             view.requireViewById(R.id.keyguard_indication_text_bottom)
67 
68         view.clipChildren = false
69         view.clipToPadding = false
70 
71         val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
72         disposables +=
73             view.repeatWhenAttached {
74                 repeatOnLifecycle(Lifecycle.State.STARTED) {
75                     launch("$TAG#viewModel.indicationAreaTranslationX") {
76                         viewModel.indicationAreaTranslationX.collect { translationX ->
77                             view.translationX = translationX
78                         }
79                     }
80 
81                     launch("$TAG#viewModel.isIndicationAreaPadded") {
82                         combine(
83                                 viewModel.isIndicationAreaPadded,
84                                 configurationBasedDimensions.map { it.indicationAreaPaddingPx },
85                             ) { isPadded, paddingIfPaddedPx ->
86                                 if (isPadded) {
87                                     paddingIfPaddedPx
88                                 } else {
89                                     0
90                                 }
91                             }
92                             .collect { paddingPx -> view.setPadding(paddingPx, 0, paddingPx, 0) }
93                     }
94 
95                     launch("$TAG#viewModel.indicationAreaTranslationY") {
96                         configurationBasedDimensions
97                             .map { it.defaultBurnInPreventionYOffsetPx }
98                             .flatMapLatest { defaultBurnInOffsetY ->
99                                 viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
100                             }
101                             .collect { translationY -> view.translationY = translationY }
102                     }
103 
104                     launch("$TAG#indicationText.setTextSize") {
105                         configurationBasedDimensions.collect { dimensions ->
106                             indicationText.setTextSize(
107                                 TypedValue.COMPLEX_UNIT_PX,
108                                 dimensions.indicationTextSizePx.toFloat(),
109                             )
110                             indicationTextBottom.setTextSize(
111                                 TypedValue.COMPLEX_UNIT_PX,
112                                 dimensions.indicationTextSizePx.toFloat(),
113                             )
114                         }
115                     }
116 
117                     launch("$TAG#viewModel.configurationChange") {
118                         viewModel.configurationChange.collect {
119                             configurationBasedDimensions.value = loadFromResources(view)
120                             if (Flags.indicationTextA11yFix()) {
121                                 indicationController.onConfigurationChanged()
122                             }
123                         }
124                     }
125 
126                     launch("$TAG#viewModel.visible") {
127                         viewModel.visible.collect { visible ->
128                             indicationController.setVisible(visible)
129                         }
130                     }
131                 }
132             }
133         return disposables
134     }
135 
136     private fun loadFromResources(view: View): ConfigurationBasedDimensions {
137         return ConfigurationBasedDimensions(
138             defaultBurnInPreventionYOffsetPx =
139                 view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset),
140             indicationAreaPaddingPx =
141                 view.resources.getDimensionPixelOffset(R.dimen.keyguard_indication_area_padding),
142             indicationTextSizePx =
143                 view.resources.getDimensionPixelSize(
144                     com.android.internal.R.dimen.text_size_small_material
145                 ),
146         )
147     }
148 
149     private data class ConfigurationBasedDimensions(
150         val defaultBurnInPreventionYOffsetPx: Int,
151         val indicationAreaPaddingPx: Int,
152         val indicationTextSizePx: Int,
153     )
154 
155     private const val TAG = "KeyguardIndicationAreaBinder"
156 }
157