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