• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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