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.biometrics.ui.binder
18
19 import android.content.Context
20 import android.content.res.Resources
21 import android.hardware.biometrics.PromptContentItem
22 import android.hardware.biometrics.PromptContentItemBulletedText
23 import android.hardware.biometrics.PromptContentItemPlainText
24 import android.hardware.biometrics.PromptContentView
25 import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
26 import android.hardware.biometrics.PromptVerticalListContentView
27 import android.text.SpannableString
28 import android.text.Spanned
29 import android.text.TextPaint
30 import android.text.style.BulletSpan
31 import android.view.LayoutInflater
32 import android.view.View
33 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
34 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
35 import android.view.ViewTreeObserver
36 import android.widget.Button
37 import android.widget.LinearLayout
38 import android.widget.Space
39 import android.widget.TextView
40 import com.android.settingslib.Utils
41 import com.android.systemui.biometrics.ui.BiometricPromptLayout
42 import com.android.systemui.lifecycle.repeatWhenAttached
43 import com.android.systemui.res.R
44 import kotlin.math.ceil
45
46 private const val TAG = "BiometricCustomizedViewBinder"
47
48 /** Sub-binder for [BiometricPromptLayout.customized_view_container]. */
49 object BiometricCustomizedViewBinder {
50 fun bind(
51 customizedViewContainer: LinearLayout,
52 contentView: PromptContentView?,
53 legacyCallback: Spaghetti.Callback
54 ) {
55 customizedViewContainer.repeatWhenAttached { containerView ->
56 if (contentView == null) {
57 containerView.visibility = View.GONE
58 return@repeatWhenAttached
59 }
60
61 containerView.width { containerWidth ->
62 if (containerWidth == 0) {
63 return@width
64 }
65 (containerView as LinearLayout).addView(
66 contentView.toView(containerView.context, containerWidth, legacyCallback),
67 LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
68 )
69 containerView.visibility = View.VISIBLE
70 }
71 }
72 }
73 }
74
toViewnull75 private fun PromptContentView.toView(
76 context: Context,
77 containerViewWidth: Int,
78 legacyCallback: Spaghetti.Callback
79 ): View {
80 return when (this) {
81 is PromptVerticalListContentView -> initLayout(context, containerViewWidth)
82 is PromptContentViewWithMoreOptionsButton -> initLayout(context, legacyCallback)
83 else -> {
84 throw IllegalStateException("No such PromptContentView: $this")
85 }
86 }
87 }
88
LayoutInflaternull89 private fun LayoutInflater.inflateContentView(id: Int, description: String?): LinearLayout {
90 val contentView = inflate(id, null) as LinearLayout
91
92 val descriptionView = contentView.requireViewById<TextView>(R.id.customized_view_description)
93 if (!description.isNullOrEmpty()) {
94 descriptionView.text = description
95 } else {
96 descriptionView.visibility = View.GONE
97 }
98 return contentView
99 }
100
PromptContentViewWithMoreOptionsButtonnull101 private fun PromptContentViewWithMoreOptionsButton.initLayout(
102 context: Context,
103 legacyCallback: Spaghetti.Callback
104 ): View {
105 val inflater = LayoutInflater.from(context)
106 val contentView =
107 inflater.inflateContentView(
108 R.layout.biometric_prompt_content_with_button_layout,
109 description
110 )
111 val buttonView = contentView.requireViewById<Button>(R.id.customized_view_more_options_button)
112 buttonView.setOnClickListener { legacyCallback.onContentViewMoreOptionsButtonPressed() }
113 return contentView
114 }
115
PromptVerticalListContentViewnull116 private fun PromptVerticalListContentView.initLayout(
117 context: Context,
118 containerViewWidth: Int
119 ): View {
120 val inflater = LayoutInflater.from(context)
121 context.resources
122 val contentView =
123 inflater.inflateContentView(
124 R.layout.biometric_prompt_vertical_list_content_layout,
125 description
126 )
127 val listItemsToShow = ArrayList<PromptContentItem>(listItems)
128 // Show two column by default, once there is an item exceeding max lines, show single
129 // item instead.
130 val showTwoColumn =
131 listItemsToShow.all { !it.doesExceedMaxLinesIfTwoColumn(context, containerViewWidth) }
132 // If should show two columns and there are more than one items, make listItems always have odd
133 // number items.
134 if (showTwoColumn && listItemsToShow.size > 1 && listItemsToShow.size % 2 == 1) {
135 listItemsToShow.add(dummyItem())
136 }
137 var currRow = createNewRowLayout(inflater)
138 for (i in 0 until listItemsToShow.size) {
139 val item = listItemsToShow[i]
140 val itemView = item.toView(context, inflater)
141 contentView.removeTopPaddingForFirstRow(description, itemView)
142
143 // If there should be two column, and there is already one item in the current row, add
144 // space between two items.
145 if (showTwoColumn && currRow.childCount == 1) {
146 currRow.addSpaceViewBetweenListItem()
147 }
148 currRow.addView(itemView)
149
150 // If there should be one column, or there are already two items (plus the space view) in
151 // the current row, or it's already the last item, start a new row
152 if (!showTwoColumn || currRow.childCount == 3 || i == listItemsToShow.size - 1) {
153 contentView.addView(currRow)
154 currRow = createNewRowLayout(inflater)
155 }
156 }
157 return contentView
158 }
159
createNewRowLayoutnull160 private fun createNewRowLayout(inflater: LayoutInflater): LinearLayout {
161 return inflater.inflate(R.layout.biometric_prompt_content_row_layout, null) as LinearLayout
162 }
163
PromptContentItemnull164 private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn(
165 context: Context,
166 containerViewWidth: Int,
167 ): Boolean {
168 val resources = context.resources
169 val passedInText: String =
170 when (this) {
171 is PromptContentItemPlainText -> text
172 is PromptContentItemBulletedText -> text
173 else -> {
174 throw IllegalStateException("No such PromptContentItem: $this")
175 }
176 }
177
178 when (this) {
179 is PromptContentItemPlainText,
180 is PromptContentItemBulletedText -> {
181 val contentViewPadding =
182 resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_padding_horizontal)
183 val listItemPadding = getListItemPadding(resources)
184 var maxWidth = containerViewWidth / 2 - contentViewPadding - listItemPadding
185 // Reduce maxWidth a bit since paint#measureText is not accurate. See b/330909104 for
186 // more context.
187 maxWidth -= contentViewPadding / 2
188
189 val paint = TextPaint()
190 val attributes =
191 context.obtainStyledAttributes(
192 R.style.TextAppearance_AuthCredential_ContentViewListItem,
193 intArrayOf(android.R.attr.textSize)
194 )
195 paint.textSize = attributes.getDimensionPixelSize(0, 0).toFloat()
196 val textWidth = paint.measureText(passedInText)
197 attributes.recycle()
198
199 val maxLines =
200 resources.getInteger(
201 R.integer.biometric_prompt_content_list_item_max_lines_if_two_column
202 )
203 val numLines = ceil(textWidth / maxWidth).toInt()
204 return numLines > maxLines
205 }
206 else -> {
207 throw IllegalStateException("No such PromptContentItem: $this")
208 }
209 }
210 }
211
toViewnull212 private fun PromptContentItem.toView(
213 context: Context,
214 inflater: LayoutInflater,
215 ): TextView {
216 val resources = context.resources
217 // Somehow xml layout params settings doesn't work, set it again here.
218 val textView =
219 inflater.inflate(R.layout.biometric_prompt_content_row_item_text_view, null) as TextView
220 val lp = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f)
221 textView.layoutParams = lp
222
223 when (this) {
224 is PromptContentItemPlainText -> {
225 textView.text = text
226 }
227 is PromptContentItemBulletedText -> {
228 val bulletedText = SpannableString(text)
229 val span =
230 BulletSpan(
231 getListItemBulletGapWidth(resources),
232 getListItemBulletColor(context),
233 getListItemBulletRadius(resources)
234 )
235 bulletedText.setSpan(span, 0 /* start */, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
236 textView.text = bulletedText
237 }
238 else -> {
239 throw IllegalStateException("No such PromptContentItem: $this")
240 }
241 }
242 return textView
243 }
244
245 /* [contentView] function */
addSpaceViewBetweenListItemnull246 private fun LinearLayout.addSpaceViewBetweenListItem() =
247 addView(
248 Space(context),
249 LinearLayout.LayoutParams(
250 resources.getDimensionPixelSize(
251 R.dimen.biometric_prompt_content_space_width_between_items
252 ),
253 MATCH_PARENT
254 )
255 )
256
257 /* [contentView] function*/
258 private fun LinearLayout.removeTopPaddingForFirstRow(description: String?, itemView: TextView) {
259 // If this item will be in the first row (contentView only has description view and
260 // description is empty), remove top padding of this item.
261 if (description.isNullOrEmpty() && childCount == 1) {
262 itemView.setPadding(itemView.paddingLeft, 0, itemView.paddingRight, itemView.paddingBottom)
263 }
264 }
265
dummyItemnull266 private fun dummyItem(): PromptContentItem = PromptContentItemPlainText("")
267
268 private fun PromptContentItem.getListItemPadding(resources: Resources): Int {
269 var listItemPadding =
270 resources.getDimensionPixelSize(
271 R.dimen.biometric_prompt_content_space_width_between_items
272 ) / 2
273 when (this) {
274 is PromptContentItemPlainText -> {}
275 is PromptContentItemBulletedText -> {
276 listItemPadding +=
277 getListItemBulletRadius(resources) * 2 + getListItemBulletGapWidth(resources)
278 }
279 else -> {
280 throw IllegalStateException("No such PromptContentItem: $this")
281 }
282 }
283 return listItemPadding
284 }
285
getListItemBulletRadiusnull286 private fun getListItemBulletRadius(resources: Resources): Int =
287 resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_list_item_bullet_radius)
288
289 private fun getListItemBulletGapWidth(resources: Resources): Int =
290 resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_list_item_bullet_gap_width)
291
292 private fun getListItemBulletColor(context: Context): Int =
293 Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.materialColorOnSurface)
294
295 private fun <T : View> T.width(function: (Int) -> Unit) {
296 if (width == 0)
297 viewTreeObserver.addOnGlobalLayoutListener(
298 object : ViewTreeObserver.OnGlobalLayoutListener {
299 override fun onGlobalLayout() {
300 if (measuredWidth > 0) {
301 viewTreeObserver.removeOnGlobalLayoutListener(this)
302 }
303 function(measuredWidth)
304 }
305 }
306 )
307 else function(measuredWidth)
308 }
309