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