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