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