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