运用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() }
        }
    }
}

演示效果

主动提交输入值 手动提交输入值
Android 实现自动填充功能与自定义自动填充服务
Android 实现自动填充功能与自定义自动填充服务

自定义主动填充服务

谷歌已经供给了一个主动填充服务,国内的手机厂商一般也会供给一个,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")
                })
            }
        }
    }
}

演示效果

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

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

示例

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

ExampleDemo github

ExampleDemo gitee