Android 实现自动填充功能与自定义自动填充服务

1,355 阅读7分钟

使用App时,经常会需要输入账号密码进行登录,账号密码很容易输错或者忘记。本文介绍如何使用自动填充框架实现自动填写账号密码功能与如何自定义自动填充服务。

自动填充框架

填写表单(例如账号密码)是一件相当耗时且容易出错的事。从Android8.0开始,官方提供了自动填充框架,可以让用户无需再输入重复信息(同时也降低了出错的概率),改善用户体验。

官方文档

支持自动填充

设置自动填充提示

在xml中为需要自动填充的控件添加自动填充提示,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.AppCompatEditText
        android:id="@+id/et_account"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:autofillHints="username"
        android:hint="Please enter your account"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

或者在代码中为需要自动填充的控件添加自动填充提示,代码如下:

class AutoFillExampleActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<LayoutAutofillExampleActivityBinding>(this, R.layout.layout_autofill_example_activity)
        binding.etAccount.setAutofillHints(View.AUTOFILL_HINT_USERNAME)
    }
}

自动填充提示可以设置为任意字符串,自动填充框架不会验证控件设置的提示内容。但是View类和HintConstants类中提供了官方支持的提示常量列表,组合使用官方提供的提示常量列表即可满足常见的自动填充页面的需求。

HintConstants提供的常见的提示常量有AUTOFILL_HINT_USERNAMEAUTOFILL_HINT_PASSWORDAUTOFILL_HINT_PHONE等,其余更多的常量可以在官方文档中查看。

标记是否需要自动填充

在实际使用中,可能不是所有的输入框都需要使用自动填充,例如输入验证码,验证码会改变,不需要保存旧的数据。

在xml中为控件配置是否需要自动填充,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.AppCompatEditText
        android:id="@+id/et_account"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:hint="Please enter your account"
        android:importantForAutofill="yes"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

或者在代码中为控件配置是否需要自动填充,代码如下:

class AutoFillExampleActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<LayoutAutofillExampleActivityBinding>(this, R.layout.layout_autofill_example_activity)
        binding.etAccount.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES
    }
}

importantForAutofill可以配置为下列这些值:

备注
View.IMPORTANT_FOR_AUTOFILL_AUTO由系统判断此视图是否需要自动填充
View.IMPORTANT_FOR_AUTOFILL_YES此视图需要自动填充
View.IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS此视图需要自动填充,但其子视图不需要自动填充
View.IMPORTANT_FOR_AUTOFILL_NO此视图不需要自动填充
View.IMPORTANT_FOR_AUTOFILL_AUTO此视图及其子视图不需要自动填充

提交需要保存的值

自动填充框架通常会在activity结束时弹出对话框询问用户是否由框架保存当前输入的值以供将来使用。我们也可以调用AutofillManagercommit方法主动实现这一步骤。代码如下:

class AutoFillExampleActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<LayoutAutofillExampleActivityBinding>(this, R.layout.layout_autofill_example_activity)
        val autofillManager = getSystemService(AutofillManager::class.java)
        // 先判断是否自动填充服务是否可用
        if (autofillManager.isEnabled && autofillManager.isAutofillSupported) {
            binding.btnCommit.setOnClickListener { autofillManager.commit() }
        }
    }
}

演示效果

自动提交输入值手动提交输入值
finsh -original-original.gifcommit -original-original.gif

自定义自动填充服务

谷歌已经提供了一个自动填充服务,国内的手机厂商一般也会提供一个,Edge浏览器也会提供自动填充服务。当然,我们自己也能实现一个自动填充服务。

自定义AutofillServices

自定义ExampleAutofillServices继承AutofillService,代码如下:

class ExampleAutofillServices : AutofillService() {

    override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback) {
        // 触发自动填充时回调
    }

    override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
        // 触发保存时回调
    }
}
解析AssistStructure

系统在判断需要自动填充时,调用onFillRequest方法向AutofillService中传入AssistStructureAssistStructure包含ViewNodeViewNode可以用于判断是否可以提供合适的数据。解析AssistStructure代码如下:

class ExampleAutofillServices : AutofillService() {

    // 以autofillHints为键,保存匹配的autofillId
    private val fillId = HashMap<String, AutofillId>()

    override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback) {
        parseStructure(request.fillContexts.last().structure)
    }

    override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
        parseStructure(request.fillContexts.last().structure, true)
    }

    private fun parseStructure(structure: AssistStructure, fromSaveRequest: Boolean) {
        fillId.clear()
        for (index in 0 until structure.windowNodeCount) {
            val windowNode = structure.getWindowNodeAt(index)
            windowNode.rootViewNode?.let { parseViewNode(it, fromSaveRequest) }
        }
    }

    private fun parseViewNode(viewNode: ViewNode, fromSaveRequest: Boolean) {
        viewNode.run {
            if (autofillHints.isNullOrEmpty()) {
                // 如果应用没有提供autofillHints
                // 可以根据viewNode.getText()或viewNode.getHint()来判断需要提供什么类型的数据
            } else {
                autofillHints?.forEach { hint ->
                    // 判断Hint并保存相应的id
                    // 这边以username和password为例
                    if (hint == HintConstants.AUTOFILL_HINT_USERNAME || hint == HintConstants.AUTOFILL_HINT_PASSWORD) {
                        autofillId?.let { id -> fillId[hint] = id }
                    }
                }
            }
            // 遍历viewNode包含的子Node
            for (childIndex in 0 until childCount) {
                parseViewNode(getChildAt(childIndex), fromSaveRequest)
            }
        }
    }
}
保存用户输入的值

实现保存数据功能,保存用户输入的数据,以供未来使用。

步骤如下:

  1. onFillRequest中配置SaveInfo以表明需要保存数据。
  2. onSaveRequest中保存用户输入的数据。

代码如下:

使用DataStore存取数据

object ExampleDataStore : PreferenceDataStore() {

    private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "ExamplePreferencesDataStore")

    var coroutineScope: CoroutineScope? = null

    override fun putInt(key: String?, value: Int) {
        coroutineScope?.launch {
            putIntImpl(key, value)
        }
    }

    override fun getInt(key: String?, defValue: Int): Int {
        var getValue: Int
        runBlocking {
            getValue = getIntImpl(key, defValue)
        }
        return getValue
    }

    override fun putLong(key: String?, value: Long) {
        coroutineScope?.launch {
            putLongImpl(key, value)
        }
    }

    override fun getLong(key: String?, defValue: Long): Long {
        var getValue: Long
        runBlocking {
            getValue = getLongImpl(key, defValue)
        }
        return getValue
    }

    override fun putFloat(key: String?, value: Float) {
        coroutineScope?.launch {
            putFloatImpl(key, value)
        }
    }

    override fun getFloat(key: String?, defValue: Float): Float {
        var getValue: Float
        runBlocking {
            getValue = getFloatImpl(key, defValue)
        }
        return getValue
    }

    override fun putBoolean(key: String?, value: Boolean) {
        coroutineScope?.launch {
            putBooleanImpl(key, value)
        }
    }

    override fun getBoolean(key: String?, defValue: Boolean): Boolean {
        var getValue: Boolean
        runBlocking {
            getValue = getBooleanImpl(key, defValue)
        }
        return getValue
    }

    override fun putString(key: String?, value: String?) {
        coroutineScope?.launch {
            putStringImpl(key, value)
        }
    }

    override fun getString(key: String?, defValue: String?): String? {
        var getValue: String?
        runBlocking {
            getValue = getStringImpl(key, defValue)
        }
        return getValue
    }

    override fun putStringSet(key: String?, values: MutableSet<String>?) {
        coroutineScope?.launch {
            putStringSetImpl(key, values)
        }
    }

    override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String> {
        val getValue = mutableSetOf<String>()
        runBlocking {
            getValue.addAll(getStringSetImpl(key, defValues))
        }
        return getValue
    }

    private suspend fun putIntImpl(key: String?, value: Int?) {
        if (key?.isNotEmpty() == true && value != null) {
            val preferencesKey = intPreferencesKey(key)
            ExampleApplication.exampleContext?.run {
                dataStore.edit {
                    it[preferencesKey] = value
                }
            }
        }
    }

    private suspend fun getIntImpl(key: String?, defaultValue: Int?): Int {
        return if (key?.isNotEmpty() == true) {
            val preferencesKey = intPreferencesKey(key)
            ExampleApplication.exampleContext?.dataStore?.data?.map {
                it[preferencesKey] ?: (defaultValue ?: 0)
            }?.first() ?: 0
        } else {
            0
        }
    }

    private suspend fun putLongImpl(key: String?, value: Long?) {
        if (key?.isNotEmpty() == true && value != null) {
            val preferencesKey = longPreferencesKey(key)
            ExampleApplication.exampleContext?.run {
                dataStore.edit {
                    it[preferencesKey] = value
                }
            }
        }
    }

    private suspend fun getLongImpl(key: String?, defaultValue: Long?): Long {
        return if (key?.isNotEmpty() == true) {
            val preferencesKey = longPreferencesKey(key)
            ExampleApplication.exampleContext?.dataStore?.data?.map {
                it[preferencesKey] ?: (defaultValue ?: 0L)
            }?.first() ?: 0L
        } else {
            0L
        }
    }

    private suspend fun putFloatImpl(key: String?, value: Float?) {
        if (key?.isNotEmpty() == true && value != null) {
            val preferencesKey = floatPreferencesKey(key)
            ExampleApplication.exampleContext?.run {
                dataStore.edit {
                    it[preferencesKey] = value
                }
            }
        }
    }

    private suspend fun getFloatImpl(key: String?, defaultValue: Float?): Float {
        return if (key?.isNotEmpty() == true) {
            val preferencesKey = floatPreferencesKey(key)
            ExampleApplication.exampleContext?.dataStore?.data?.map {
                it[preferencesKey] ?: (defaultValue ?: 0f)
            }?.first() ?: 0f
        } else {
            0f
        }
    }

    private suspend fun putBooleanImpl(key: String?, value: Boolean?) {
        if (key?.isNotEmpty() == true && value != null) {
            val preferencesKey = booleanPreferencesKey(key)
            ExampleApplication.exampleContext?.run {
                dataStore.edit {
                    it[preferencesKey] = value
                }
            }
        }
    }

    private suspend fun getBooleanImpl(key: String?, defaultValue: Boolean?): Boolean {
        return if (key?.isNotEmpty() == true) {
            val preferencesKey = booleanPreferencesKey(key)
            ExampleApplication.exampleContext?.dataStore?.data?.map {
                it[preferencesKey] ?: (defaultValue ?: false)
            }?.first() ?: false
        } else {
            false
        }
    }

    private suspend fun putStringImpl(key: String?, value: String?) {
        if (key?.isNotEmpty() == true && value?.isNotEmpty() == true) {
            val preferencesKey = stringPreferencesKey(key)
            ExampleApplication.exampleContext?.run {
                dataStore.edit {
                    it[preferencesKey] = value
                }
            }
        }
    }

    private suspend fun getStringImpl(key: String?, defaultValue: String?): String? {
        return if (key?.isNotEmpty() == true) {
            val preferencesKey = stringPreferencesKey(key)
            ExampleApplication.exampleContext?.dataStore?.data?.map {
                it[preferencesKey] ?: (defaultValue ?: "")
            }?.first()
        } else {
            ""
        }
    }

    private suspend fun putStringSetImpl(key: String?, value: Set<String>?) {
        if (key?.isNotEmpty() == true && value?.isNotEmpty() == true) {
            val preferencesKey = stringSetPreferencesKey(key)
            ExampleApplication.exampleContext?.run {
                dataStore.edit {
                    it[preferencesKey] = value
                }
            }
        }
    }

    private suspend fun getStringSetImpl(key: String?, defaultValue: Set<String>?): Set<String> {
        return if (key?.isNotEmpty() == true) {
            val preferencesKey = stringSetPreferencesKey(key)
            ExampleApplication.exampleContext?.dataStore?.data?.map {
                it[preferencesKey] ?: (defaultValue ?: setOf())
            }?.first() ?: setOf()
        } else {
            setOf()
        }
    }
}

调整ExampleAutofillServices

class ExampleAutofillServices : AutofillService() {

    private val fillId = HashMap<String, AutofillId>()

    private val saveInputValues = HashMap<String, String>()
    
    override fun onCreate() {
        super.onCreate()
        ExampleDataStore.coroutineScope = CoroutineScope(Dispatchers.IO)
    }

    override fun onDestroy() {
        super.onDestroy()
        ExampleDataStore.coroutineScope?.cancel()
        ExampleDataStore.coroutineScope = null
    }
    
    override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback) {
        request.fillContexts.last().structure.let { structure ->
            parseStructure(structure)
            val usernameId = fillId[HintConstants.AUTOFILL_HINT_USERNAME]
            val passwordId = fillId[HintConstants.AUTOFILL_HINT_PASSWORD]
            if (usernameId != null && passwordId != null) {                   
                callback.onSuccess(FillResponse.Builder().setSaveInfo(
                   SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_USERNAME or SaveInfo.SAVE_DATA_TYPE_PASSWORD,
                       arrayOf(usernameId, passwordId))
                   .build()))
            }
        }
    }

    override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
        request.fillContexts.last().structure.let { structure ->
            saveInputValues.clear()
            parseStructure(structure, true)
            if (saveInputValues.isNotEmpty()) {
                saveInputValues.entries.forEach {
                    // 这里以控件所在的类的类名和Hint为键,保存用户输入的值做示例
                    ExampleDataStore.putString("${structure.activityComponent.shortClassName}_${it.key}", it.value)
                }
                callback.onSuccess()
            } else {
                callback.onFailure("fill value not found")
            }
        }
    }

    private fun parseStructure(structure: AssistStructure, fromSaveRequest: Boolean = false) {
        fillId.clear()
        for (index in 0 until structure.windowNodeCount) {
            val windowNode = structure.getWindowNodeAt(index)
            windowNode.rootViewNode?.let { parseViewNode(it, fromSaveRequest) }
        }
    }

    private fun parseViewNode(viewNode: ViewNode, fromSaveRequest: Boolean) {
        viewNode.run {
            if (autofillHints.isNullOrEmpty()) {
                // 如果应用没有提供autofillHints
                // 可以根据viewNode.getText()或viewNode.getHint()来判断需要提供什么类型的数据
            } else {
                autofillHints?.forEach { hint ->
                    if (hint == HintConstants.AUTOFILL_HINT_USERNAME || hint == HintConstants.AUTOFILL_HINT_PASSWORD) {
                        autofillId?.let { id -> fillId[hint] = id }
                        if (fromSaveRequest) {
                            autofillValue?.let { value -> saveInputValues[hint] = value.textValue.toString() }
                        }
                    }
                }
            }
            for (childIndex in 0 until childCount) {
                parseViewNode(getChildAt(childIndex), fromSaveRequest)
            }
        }
    }
}
提供自动填充内容

需要自动填充时,自动填充服务会收到请求,如果能提供合适的数据,则可以配置到回调中。

onFillRequest中配置Dataset,并添加到回调中。ExampleAutofillServices调整代码如下:

class ExampleAutofillServices : AutofillService() {

    ....

    override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback) {
        request.fillContexts.last().structure.let { structure ->
            parseStructure(structure)
            val usernameId = fillId[HintConstants.AUTOFILL_HINT_USERNAME]
            val passwordId = fillId[HintConstants.AUTOFILL_HINT_PASSWORD]
            if (usernameId != null && passwordId != null) {
                val fillResponseBuilder = FillResponse.Builder()
                val usernameSavedValue = ExampleDataStore.getString("${structure.activityComponent.shortClassName}_${HintConstants.AUTOFILL_HINT_USERNAME}", "")
                val passwordSavedValue = ExampleDataStore.getString("${structure.activityComponent.shortClassName}_${HintConstants.AUTOFILL_HINT_PASSWORD}", "")
                if (TextUtils.isEmpty(usernameSavedValue) && TextUtils.isEmpty(passwordSavedValue)) {
                    fillResponseBuilder.setSaveInfo(SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_USERNAME or SaveInfo.SAVE_DATA_TYPE_PASSWORD,
                        arrayOf(usernameId, passwordId))
                        .build())
                } else {
                    val dataSetBuilder = Dataset.Builder()
                    var saveInfo: SaveInfo? = null
                    if (!TextUtils.isEmpty(usernameSavedValue)) {
                        val usernamePresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1).apply {
                            setTextViewText(android.R.id.text1, "Account")
                        }
                        dataSetBuilder.setValue(usernameId, AutofillValue.forText(usernameSavedValue), usernamePresentation)
                        if (TextUtils.isEmpty(passwordSavedValue)) {
                            saveInfo = SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD, arrayOf(passwordId)).build()
                        }
                    }
                    if (!TextUtils.isEmpty(passwordSavedValue)) {
                        val passwordPresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1).apply {
                            setTextViewText(android.R.id.text1, "Password")
                        }
                        dataSetBuilder.setValue(passwordId, AutofillValue.forText(passwordSavedValue), passwordPresentation)
                        if (TextUtils.isEmpty(usernameSavedValue)) {
                            saveInfo = SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_USERNAME, arrayOf(usernameId)).build()
                        }
                    }
                    fillResponseBuilder.addDataset(dataSetBuilder.build())
                    saveInfo?.let { fillResponseBuilder.setSaveInfo(it) }
                }
                callback.onSuccess(fillResponseBuilder.build())
            }
        }
    }

    ...
}

在Manifest中注册AutofillServices

AndroidManifest中添加ExampleAutofillServices,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        ... >

        <service
            android:name=".androidapi.autofill.ExampleAutofillServices"
            android:exported="true"
            android:label="Example Autofill Service"
            android:permission="android.permission.BIND_AUTOFILL_SERVICE">

            <intent-filter>
                <action android:name="android.service.autofill.AutofillService" />
            </intent-filter>
            
        </service>
        
    </application>
</manifest>

启用AutofillServices

通过代码来提示用户使用自动填充服务,代码如下:

class AutoFillExampleActivity : AppCompatActivity() {

    private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        if (it.resultCode == Activity.RESULT_OK) {
            // 结果回调
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<LayoutAutofillExampleActivityBinding>(this, R.layout.layout_autofill_example_activity)
        val autofillManager = getSystemService(AutofillManager::class.java)
        binding.btnChangeAutofillService.setOnClickListener {
            // isAutofillSupported判断设备是否支持自动填充服务
            // hasEnabledAutofillServices判断当前使用的自动填充服务是否是我们自定义的
            if (autofillManager.isAutofillSupported && !autofillManager.hasEnabledAutofillServices()) {
                launcher.launch(Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply {
                    data = Uri.parse("package:com.chenyihong.exampledemo")
                })
            }
        }
    }
}

演示效果

device-2023-04-29-15 -middle-original.gif

可以看到,不能一次性同时保存用户名和密码,后续有空会研究如何解决这个问题。

示例

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee