AndroidAutofill.android.kt

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.ui.autofill

import android.os.Build
import android.util.Log
import android.util.SparseArray
import android.view.View
import android.view.ViewStructure
import android.view.autofill.AutofillId
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.util.fastMap

/**
 * Autofill implementation for Android.
 *
 * @param view The parent compose view.
 * @param autofillTree The autofill tree. This will be replaced by a semantic tree (b/138604305).
 */
@ExperimentalComposeUiApi
@RequiresApi(Build.VERSION_CODES.O)
internal class AndroidAutofill(val view: View, val autofillTree: AutofillTree) : Autofill {

    val autofillManager = view.context.getSystemService(AutofillManager::class.java)
        ?: error("Autofill service could not be located.")

    init { view.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES }

    override fun requestAutofillForNode(autofillNode: AutofillNode) {
        // TODO(b/138731416): Find out what happens when notifyViewEntered() is called multiple times
        // before calling notifyViewExited().
        autofillManager.notifyViewEntered(
            view,
            autofillNode.id,
            autofillNode.boundingBox?.toAndroidRect()
                ?: error("requestAutofill called before onChildPositioned()")
        )
    }

    override fun cancelAutofillForNode(autofillNode: AutofillNode) {
        autofillManager.notifyViewExited(view, autofillNode.id)
    }
}

/**
 * Populates the view structure with autofill information.
 *
 * @param root A reference to the view structure of the parent compose view.
 *
 * This function populates the view structure using the information in the { AutofillTree }.
 */
@ExperimentalComposeUiApi
@RequiresApi(Build.VERSION_CODES.O)
internal fun AndroidAutofill.populateViewStructure(root: ViewStructure) {

    // Add child nodes. The function returns the index to the first item.
    var index = AutofillApi23Helper.addChildCount(root, autofillTree.children.count())

    for ((id, autofillNode) in autofillTree.children) {
        AutofillApi23Helper.newChild(root, index)?.also { child ->
            AutofillApi26Helper.setAutofillId(
                child,
                AutofillApi26Helper.getAutofillId(root)!!,
                id
            )
            AutofillApi23Helper.setId(child, id, view.context.packageName, null, null)
            AutofillApi26Helper.setAutofillType(child, View.AUTOFILL_TYPE_TEXT)
            AutofillApi26Helper.setAutofillHints(
                child,
                autofillNode.autofillTypes.fastMap { it.androidType }.toTypedArray()
            )

            if (autofillNode.boundingBox == null) {
                // Do we need an exception here? warning? silently ignore? If the boundingbox is
                // null, the autofill overlay will not be shown.
                Log.w(
                    "Autofill Warning",
                    """Bounding box not set.
                        Did you call perform autofillTree before the component was positioned? """
                )
            }
            autofillNode.boundingBox?.toAndroidRect()?.run {
                AutofillApi23Helper.setDimens(child, left, top, 0, 0, width(), height())
            }
        }
        index++
    }
}

/**
 * Triggers onFill() in response to a request from the autofill framework.
 */
@ExperimentalComposeUiApi
@RequiresApi(Build.VERSION_CODES.O)
internal fun AndroidAutofill.performAutofill(values: SparseArray<AutofillValue>) {
    for (index in 0 until values.size()) {
        val itemId = values.keyAt(index)
        val value = values[itemId]
        when {
            AutofillApi26Helper.isText(value) -> autofillTree.performAutofill(
                itemId,
                AutofillApi26Helper.textValue(value).toString()
            )
            AutofillApi26Helper.isDate(value) ->
                TODO("b/138604541: Add onFill() callback for date")
            AutofillApi26Helper.isList(value) ->
                TODO("b/138604541: Add onFill() callback for list")
            AutofillApi26Helper.isToggle(value) ->
                TODO("b/138604541:  Add onFill() callback for toggle")
        }
    }
}

/**
 * This class is here to ensure that the classes that use this API will get verified and can be
 * AOT compiled. It is expected that this class will soft-fail verification, but the classes
 * which use this method will pass.
 */
@RequiresApi(26)
internal object AutofillApi26Helper {
    @RequiresApi(26)
    @DoNotInline
    fun setAutofillId(structure: ViewStructure, parent: AutofillId, virtualId: Int) =
        structure.setAutofillId(parent, virtualId)

    @RequiresApi(26)
    @DoNotInline
    fun getAutofillId(structure: ViewStructure) = structure.autofillId

    @RequiresApi(26)
    @DoNotInline
    fun setAutofillType(structure: ViewStructure, type: Int) = structure.setAutofillType(type)

    @RequiresApi(26)
    @DoNotInline
    fun setAutofillHints(structure: ViewStructure, hints: Array<String>) =
        structure.setAutofillHints(hints)

    @RequiresApi(26)
    @DoNotInline
    fun isText(value: AutofillValue) = value.isText

    @RequiresApi(26)
    @DoNotInline
    fun isDate(value: AutofillValue) = value.isDate

    @RequiresApi(26)
    @DoNotInline
    fun isList(value: AutofillValue) = value.isList

    @RequiresApi(26)
    @DoNotInline
    fun isToggle(value: AutofillValue) = value.isToggle

    @RequiresApi(26)
    @DoNotInline
    fun textValue(value: AutofillValue): CharSequence = value.textValue
}

/**
 * This class is here to ensure that the classes that use this API will get verified and can be
 * AOT compiled. It is expected that this class will soft-fail verification, but the classes
 * which use this method will pass.
 */
@RequiresApi(23)
internal object AutofillApi23Helper {
    @RequiresApi(23)
    @DoNotInline
    fun newChild(structure: ViewStructure, index: Int): ViewStructure? =
        structure.newChild(index)

    @RequiresApi(23)
    @DoNotInline
    fun addChildCount(structure: ViewStructure, num: Int) =
        structure.addChildCount(num)

    @RequiresApi(23)
    @DoNotInline
    fun setId(
        structure: ViewStructure,
        id: Int,
        packageName: String?,
        typeName: String?,
        entryName: String?
    ) = structure.setId(id, packageName, typeName, entryName)

    @RequiresApi(23)
    @DoNotInline
    fun setDimens(
        structure: ViewStructure,
        left: Int,
        top: Int,
        scrollX: Int,
        scrollY: Int,
        width: Int,
        height: Int
    ) = structure.setDimens(left, top, scrollX, scrollY, width, height)
}