1 /*
<lambda>null2 * Copyright 2019 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 androidx.compose.ui.autofill
18
19 import android.os.Build
20 import android.util.Log
21 import android.util.SparseArray
22 import android.view.View
23 import android.view.ViewStructure
24 import android.view.autofill.AutofillId
25 import android.view.autofill.AutofillManager
26 import android.view.autofill.AutofillValue
27 import androidx.annotation.RequiresApi
28 import androidx.compose.ui.internal.checkPreconditionNotNull
29 import androidx.compose.ui.platform.coreshims.ViewCompatShims
30 import androidx.compose.ui.util.fastMap
31 import androidx.compose.ui.util.fastRoundToInt
32
33 /**
34 * Autofill implementation for Android.
35 *
36 * @param view The parent compose view.
37 * @param autofillTree The autofill tree. This will be replaced by a semantic tree (b/138604305).
38 */
39 @RequiresApi(Build.VERSION_CODES.O)
40 internal class AndroidAutofill(
41 val view: View,
42 val autofillTree: @Suppress("Deprecation") AutofillTree
43 ) : @Suppress("Deprecation") Autofill {
44
45 val autofillManager =
46 view.context.getSystemService(AutofillManager::class.java)
47 ?: error("Autofill service could not be located.")
48 var rootAutofillId: AutofillId
49
50 init {
51 view.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES
52 rootAutofillId =
53 checkPreconditionNotNull(ViewCompatShims.getAutofillId(view)?.toAutofillId())
54 }
55
56 override fun requestAutofillForNode(autofillNode: @Suppress("Deprecation") AutofillNode) {
57 val boundingBox =
58 autofillNode.boundingBox ?: error("requestAutofill called before onChildPositioned()")
59
60 // TODO(b/138731416): Find out what happens when notifyViewEntered() is called multiple
61 // times
62 // before calling notifyViewExited().
63 autofillManager.notifyViewEntered(
64 view,
65 autofillNode.id,
66 android.graphics.Rect(
67 boundingBox.left.fastRoundToInt(),
68 boundingBox.top.fastRoundToInt(),
69 boundingBox.right.fastRoundToInt(),
70 boundingBox.bottom.fastRoundToInt()
71 )
72 )
73 }
74
75 override fun cancelAutofillForNode(autofillNode: @Suppress("Deprecation") AutofillNode) {
76 autofillManager.notifyViewExited(view, autofillNode.id)
77 }
78 }
79
80 /**
81 * Populates the view structure with autofill information.
82 *
83 * @param root A reference to the view structure of the parent compose view.
84 *
85 * This function populates the view structure using the information in the { AutofillTree }.
86 */
87 @RequiresApi(Build.VERSION_CODES.O)
populateViewStructurenull88 internal fun AndroidAutofill.populateViewStructure(root: ViewStructure) {
89 if (autofillTree.children.isEmpty()) return
90
91 // Add child nodes. The function returns the index to the first item.
92 var index = AutofillApi26Helper.addChildCount(root, autofillTree.children.count())
93
94 for ((id, autofillNode) in autofillTree.children) {
95 AutofillApi26Helper.newChild(root, index).also { child ->
96 AutofillApi26Helper.setAutofillId(child, rootAutofillId, id)
97 AutofillApi26Helper.setId(child, id, view.context.packageName, null, null)
98 AutofillApi26Helper.setAutofillType(child, ContentDataType.Text.dataType)
99 AutofillApi26Helper.setAutofillHints(
100 child,
101 autofillNode.autofillTypes.fastMap { it.androidType }.toTypedArray()
102 )
103
104 val boundingBox = autofillNode.boundingBox
105 if (boundingBox == null) {
106 // Do we need an exception here? warning? silently ignore? If the bounding box is
107 // null, the autofill overlay will not be shown.
108 Log.w(
109 "Autofill Warning",
110 """Bounding box not set.
111 Did you call perform autofillTree before the component was positioned? """
112 )
113 } else {
114 val left = boundingBox.left.fastRoundToInt()
115 val top = boundingBox.top.fastRoundToInt()
116 val right = boundingBox.right.fastRoundToInt()
117 val bottom = boundingBox.bottom.fastRoundToInt()
118 val width = right - left
119 val height = bottom - top
120 AutofillApi26Helper.setDimens(child, left, top, 0, 0, width, height)
121 }
122 }
123 index++
124 }
125 }
126
127 /** Triggers onFill() in response to a request from the autofill framework. */
128 @RequiresApi(Build.VERSION_CODES.O)
performAutofillnull129 internal fun AndroidAutofill.performAutofill(values: SparseArray<AutofillValue>) {
130 if (autofillTree.children.isEmpty()) return
131
132 for (index in 0 until values.size()) {
133 val itemId = values.keyAt(index)
134 val value = values[itemId]
135 when {
136 AutofillApi26Helper.isText(value) ->
137 autofillTree.performAutofill(
138 itemId,
139 AutofillApi26Helper.textValue(value).toString()
140 )
141 AutofillApi26Helper.isDate(value) -> TODO("b/138604541: Add onFill() callback for date")
142 AutofillApi26Helper.isList(value) -> TODO("b/138604541: Add onFill() callback for list")
143 AutofillApi26Helper.isToggle(value) ->
144 TODO("b/138604541: Add onFill() callback for toggle")
145 }
146 }
147 }
148