1 package com.android.systemui.biometrics.ui 2 3 import android.content.Context 4 import android.content.res.Configuration.ORIENTATION_LANDSCAPE 5 import android.text.TextUtils 6 import android.util.AttributeSet 7 import android.view.View 8 import android.view.WindowInsets 9 import android.view.WindowInsets.Type.ime 10 import android.view.accessibility.AccessibilityManager 11 import android.widget.ImageView 12 import android.widget.ImeAwareEditText 13 import android.widget.LinearLayout 14 import android.widget.TextView 15 import androidx.core.view.isGone 16 import com.android.systemui.R 17 import com.android.systemui.biometrics.AuthPanelController 18 import com.android.systemui.biometrics.ui.binder.CredentialViewBinder 19 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel 20 21 /** PIN or password credential view for BiometricPrompt. */ 22 class CredentialPasswordView(context: Context, attrs: AttributeSet?) : 23 LinearLayout(context, attrs), CredentialView, View.OnApplyWindowInsetsListener { 24 25 private lateinit var titleView: TextView 26 private lateinit var subtitleView: TextView 27 private lateinit var descriptionView: TextView 28 private lateinit var iconView: ImageView 29 private lateinit var passwordField: ImeAwareEditText 30 private lateinit var credentialHeader: View 31 private lateinit var credentialInput: View 32 33 private var bottomInset: Int = 0 34 35 private val accessibilityManager by lazy { 36 context.getSystemService(AccessibilityManager::class.java) 37 } 38 39 /** Initializes the view. */ 40 override fun init( 41 viewModel: CredentialViewModel, 42 host: CredentialView.Host, 43 panelViewController: AuthPanelController, 44 animatePanel: Boolean, 45 ) { 46 CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel) 47 } 48 49 override fun onFinishInflate() { 50 super.onFinishInflate() 51 52 titleView = requireViewById(R.id.title) 53 subtitleView = requireViewById(R.id.subtitle) 54 descriptionView = requireViewById(R.id.description) 55 iconView = requireViewById(R.id.icon) 56 subtitleView = requireViewById(R.id.subtitle) 57 passwordField = requireViewById(R.id.lockPassword) 58 credentialHeader = requireViewById(R.id.auth_credential_header) 59 credentialInput = requireViewById(R.id.auth_credential_input) 60 61 setOnApplyWindowInsetsListener(this) 62 } 63 64 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { 65 super.onLayout(changed, left, top, right, bottom) 66 67 val inputLeftBound: Int 68 var inputTopBound: Int 69 var headerRightBound = right 70 var headerTopBounds = top 71 var headerBottomBounds = bottom 72 val subTitleBottom: Int = if (subtitleView.isGone) titleView.bottom else subtitleView.bottom 73 val descBottom = if (descriptionView.isGone) subTitleBottom else descriptionView.bottom 74 if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) { 75 inputTopBound = (bottom - credentialInput.height) / 2 76 inputLeftBound = (right - left) / 2 77 headerRightBound = inputLeftBound 78 if (descriptionView.bottom > headerBottomBounds) { 79 headerTopBounds -= iconView.bottom.coerceAtMost(bottomInset) 80 credentialHeader.layout(left, headerTopBounds, headerRightBound, bottom) 81 } 82 } else { 83 inputTopBound = descBottom + (bottom - descBottom - credentialInput.height) / 2 84 inputLeftBound = (right - left - credentialInput.width) / 2 85 86 if (bottom - inputTopBound < credentialInput.height) { 87 inputTopBound = bottom - credentialInput.height 88 } 89 90 if (descriptionView.bottom > inputTopBound) { 91 credentialHeader.layout(left, headerTopBounds, headerRightBound, inputTopBound) 92 } 93 } 94 95 credentialInput.layout(inputLeftBound, inputTopBound, right, bottom) 96 } 97 98 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 99 super.onMeasure(widthMeasureSpec, heightMeasureSpec) 100 101 val newWidth = MeasureSpec.getSize(widthMeasureSpec) 102 val newHeight = MeasureSpec.getSize(heightMeasureSpec) - bottomInset 103 104 setMeasuredDimension(newWidth, newHeight) 105 106 val halfWidthSpec = MeasureSpec.makeMeasureSpec(width / 2, MeasureSpec.AT_MOST) 107 val fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED) 108 if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) { 109 measureChildren(halfWidthSpec, fullHeightSpec) 110 } else { 111 measureChildren(widthMeasureSpec, fullHeightSpec) 112 } 113 } 114 115 override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets { 116 val bottomInsets = insets.getInsets(ime()) 117 if (bottomInset != bottomInsets.bottom) { 118 bottomInset = bottomInsets.bottom 119 120 if (bottomInset > 0 && resources.configuration.orientation == ORIENTATION_LANDSCAPE) { 121 titleView.isSingleLine = true 122 titleView.ellipsize = TextUtils.TruncateAt.MARQUEE 123 titleView.marqueeRepeatLimit = -1 124 // select to enable marquee unless a screen reader is enabled 125 titleView.isSelected = accessibilityManager?.shouldMarquee() ?: false 126 } else { 127 titleView.isSingleLine = false 128 titleView.ellipsize = null 129 // select to enable marquee unless a screen reader is enabled 130 titleView.isSelected = false 131 } 132 133 requestLayout() 134 } 135 return insets 136 } 137 } 138 139 private fun AccessibilityManager.shouldMarquee(): Boolean = !isEnabled || !isTouchExplorationEnabled 140