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
<lambda>null35 private val accessibilityManager by lazy {
36 context.getSystemService(AccessibilityManager::class.java)
37 }
38
39 /** Initializes the view. */
initnull40 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
onFinishInflatenull49 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
onLayoutnull64 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
onMeasurenull98 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
onApplyWindowInsetsnull115 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
AccessibilityManagernull139 private fun AccessibilityManager.shouldMarquee(): Boolean = !isEnabled || !isTouchExplorationEnabled
140