• 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.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