refactor: Use modern Autofill dataset builder on newer Android versions

This commit is contained in:
Artem Chepurnyi 2024-12-14 17:58:49 +02:00
parent 5d20685465
commit 9fa53ea157
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
4 changed files with 218 additions and 110 deletions

View File

@ -7,9 +7,7 @@ import android.os.Bundle
import android.os.Parcelable
import android.service.autofill.Dataset
import android.view.View
import android.view.autofill.AutofillId
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -33,11 +31,10 @@ import androidx.compose.ui.unit.dp
import com.artemchep.keyguard.AppMode
import com.artemchep.keyguard.LocalAppMode
import com.artemchep.keyguard.android.autofill.AutofillStructure2
import com.artemchep.keyguard.android.autofill.DatasetBuilder
import com.artemchep.keyguard.common.R
import com.artemchep.keyguard.common.io.bind
import com.artemchep.keyguard.common.model.AutofillHint
import com.artemchep.keyguard.common.model.DSecret
import com.artemchep.keyguard.common.model.gett
import com.artemchep.keyguard.common.usecase.GetTotpCode
import com.artemchep.keyguard.pick
import com.artemchep.keyguard.platform.recordLog
@ -83,12 +80,10 @@ class AutofillActivity : BaseActivity(), DIAware {
}
private fun tryBuildDataset(
index: Int,
context: Context,
secret: DSecret,
forceAddUri: Boolean,
struct: AutofillStructure2,
onComplete: (Dataset.Builder.() -> Unit)? = null,
): Dataset? {
val views = RemoteViews(context.packageName, R.layout.item_autofill_entry).apply {
setTextViewText(R.id.autofill_entry_name, secret.name)
@ -106,27 +101,25 @@ class AutofillActivity : BaseActivity(), DIAware {
setViewVisibility(R.id.autofill_entry_username, View.GONE)
}
}
val fields = runBlocking {
DatasetBuilder.fieldsStructData(
cipher = secret,
structItems = struct.items,
getTotpCode = getTotpCode,
)
}
fun createDatasetBuilder(): Dataset.Builder {
val builder = Dataset.Builder(views)
val builder = DatasetBuilder.create(
menuPresentation = views,
fields = DatasetBuilder.fields(
structItems = struct.items,
structData = fields,
),
// we are not rendering those anyway
provideInlinePresentation = { null },
)
builder.setId(secret.id)
val fields = runBlocking {
val hints = struct.items
.asSequence()
.map { it.hint }
.toSet()
secret.gett(
hints = hints,
getTotpCode = getTotpCode,
).bind()
}
struct.items.forEach { structItem ->
val value = fields[structItem.hint]
builder.trySetValue(
id = structItem.id,
value = value,
)
}
return builder
}
@ -140,9 +133,10 @@ class AutofillActivity : BaseActivity(), DIAware {
forceAddUri = forceAddUri,
structure = struct,
)
val code = PendingIntents.autofill.obtainId()
val pi = PendingIntent.getActivity(
this,
1500 + index,
code,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT,
)
@ -152,8 +146,6 @@ class AutofillActivity : BaseActivity(), DIAware {
// Ignored
}
onComplete?.invoke(builder)
return try {
builder.build()
} catch (e: Exception) {
@ -161,15 +153,6 @@ class AutofillActivity : BaseActivity(), DIAware {
}
}
private fun Dataset.Builder.trySetValue(
id: AutofillId?,
value: String?,
) {
if (id != null && value != null) {
setValue(id, AutofillValue.forText(value))
}
}
private fun autofill(
secret: DSecret,
forceAddUri: Boolean,
@ -177,7 +160,6 @@ class AutofillActivity : BaseActivity(), DIAware {
val struct = args.autofillStructure2
?: return
val dataset = tryBuildDataset(
index = 555,
context = this,
secret = secret,
forceAddUri = forceAddUri,

View File

@ -5,7 +5,7 @@ import com.artemchep.keyguard.android.downloader.NotificationIdPool
object PendingIntents {
val autofill = NotificationIdPool.sequential(0)
val credential = NotificationIdPool.sequential(
start = 100000,
endExclusive = 200000,
start = 1000000,
endExclusive = 1100000,
)
}

View File

@ -0,0 +1,126 @@
package com.artemchep.keyguard.android.autofill
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.Field
import android.service.autofill.InlinePresentation
import android.service.autofill.Presentations
import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import com.artemchep.keyguard.common.io.bind
import com.artemchep.keyguard.common.model.AutofillHint
import com.artemchep.keyguard.common.model.DSecret
import com.artemchep.keyguard.common.model.gett
import com.artemchep.keyguard.common.usecase.GetTotpCode
object DatasetBuilder {
class FieldData(
val value: String? = null,
)
suspend fun fieldsStructData(
cipher: DSecret,
structItems: List<AutofillStructure2.Item>,
getTotpCode: GetTotpCode,
) = kotlin.run {
val hints = structItems
.asSequence()
.map { it.hint }
.toSet()
cipher.gett(
hints = hints,
getTotpCode = getTotpCode,
).bind()
}
fun fields(
structItems: List<AutofillStructure2.Item>,
structData: Map<AutofillHint, String>,
) = structItems
.asSequence()
.mapNotNull { structItem ->
val autofillId = structItem.id
val autofillValue = structData[structItem.hint]
?: return@mapNotNull null
val data = FieldData(
value = autofillValue,
)
autofillId to data
}
.toMap()
inline fun create(
menuPresentation: RemoteViews,
fields: Map<AutofillId, FieldData?>,
provideInlinePresentation: () -> InlinePresentation?,
): Dataset.Builder {
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
createSdkPostTiramisu(
menuPresentation = menuPresentation,
fields = fields,
provideInlinePresentation = provideInlinePresentation,
)
} else {
createSdkPreTiramisu(
menuPresentation = menuPresentation,
fields = fields,
provideInlinePresentation = provideInlinePresentation,
)
}
return builder
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
inline fun createSdkPostTiramisu(
menuPresentation: RemoteViews,
fields: Map<AutofillId, FieldData?>,
provideInlinePresentation: () -> InlinePresentation?,
): Dataset.Builder {
val presentations = Presentations.Builder().apply {
setMenuPresentation(menuPresentation)
val inlinePresentation = provideInlinePresentation()
inlinePresentation?.let(::setInlinePresentation)
}.build()
return Dataset.Builder(presentations).apply {
fields.forEach { (autofillId, fieldData) ->
val field = if (fieldData != null) {
Field.Builder().apply {
if (fieldData.value != null) {
val autofillValue = AutofillValue.forText(fieldData.value)
setValue(autofillValue)
}
}.build()
} else {
null
}
setField(autofillId, field)
}
}
}
inline fun createSdkPreTiramisu(
menuPresentation: RemoteViews,
fields: Map<AutofillId, FieldData?>,
provideInlinePresentation: () -> InlinePresentation?,
): Dataset.Builder {
return Dataset.Builder(menuPresentation).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlinePresentation = provideInlinePresentation()
inlinePresentation?.let(::setInlinePresentation)
}
fields.forEach { (autofillId, fieldData) ->
val autofillValue = if (fieldData?.value != null) {
AutofillValue.forText(fieldData.value)
} else {
null
}
setValue(autofillId, autofillValue)
}
}
}
}

View File

@ -9,8 +9,6 @@ import android.graphics.drawable.Icon
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.*
import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
@ -22,6 +20,7 @@ import com.artemchep.keyguard.android.AutofillActivity
import com.artemchep.keyguard.android.AutofillFakeAuthActivity
import com.artemchep.keyguard.android.AutofillSaveActivity
import com.artemchep.keyguard.android.MainActivity
import com.artemchep.keyguard.android.PendingIntents
import com.artemchep.keyguard.common.R
import com.artemchep.keyguard.common.io.*
import com.artemchep.keyguard.common.model.*
@ -304,21 +303,24 @@ class KeyguardAutofillService : AutofillService(), DIAware {
var index = 0
r.value.forEach { secret ->
var datasetHasInlinePresentation = false
val dataset =
tryBuildDataset(index, this, secret, autofillStructure) {
val dataset = tryBuildDataset(
context = this,
secret = secret,
struct = autofillStructure,
provideInlinePresentation = {
if (index < secretInlineSuggestionsMaxCount) {
val inlinePresentation =
tryBuildSecretInlinePresentation(
request,
index,
secret,
)
if (inlinePresentation != null) {
setInlinePresentation(inlinePresentation)
tryBuildSecretInlinePresentation(
request,
index,
secret,
)?.also {
datasetHasInlinePresentation = true
}
} else {
null
}
}
},
)
if (dataset != null && (!shouldInlineSuggestions || datasetHasInlinePresentation)) {
responseBuilder.addDataset(dataset)
index += 1
@ -337,27 +339,31 @@ class KeyguardAutofillService : AutofillService(), DIAware {
val flags =
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
val pi = PendingIntent.getActivity(this, 1010, intent, flags)
val code = PendingIntents.autofill.obtainId()
val pi = PendingIntent.getActivity(this, code, intent, flags)
val manualSelectionView = AutofillViews
.buildPopupEntryManual(this)
autofillStructure.items.forEach {
val autofillId = it.id
val builder = Dataset.Builder(manualSelectionView)
val builder = DatasetBuilder.create(
menuPresentation = manualSelectionView,
fields = mapOf(
autofillId to null,
),
provideInlinePresentation = {
if (totalInlineSuggestionsMaxCount <= 0) {
return@create null
}
if (totalInlineSuggestionsMaxCount > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlinePresentation =
tryBuildManualSelectionInlinePresentation(
request,
index,
intent = intent,
)
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
builder.setValue(autofillId, null)
},
)
builder.setAuthentication(pi.intentSender)
responseBuilder.addDataset(builder.build())
}
@ -475,39 +481,43 @@ class KeyguardAutofillService : AutofillService(), DIAware {
autofillStructure2 = struct,
),
)
val authIntentRequestCode = PendingIntents.autofill.obtainId()
val authIntentSender = PendingIntent.getActivity(
this@KeyguardAutofillService,
1001,
authIntentRequestCode,
authIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
).intentSender
struct.items.forEach { item ->
val builder = Dataset.Builder(remoteViews)
if (canInlineSuggestions && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlinePresentation =
val builder = DatasetBuilder.create(
menuPresentation = remoteViews,
fields = mapOf(
item.id to null,
),
provideInlinePresentation = {
if (!canInlineSuggestions) {
return@create null
}
tryCreateAuthenticationInlinePresentation(
type,
request,
0,
intent = authIntent,
)
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
builder.setValue(item.id, null)
},
)
builder.setAuthentication(authIntentSender)
addDataset(builder.build())
}
}
private suspend fun tryBuildDataset(
index: Int,
context: Context,
secret: DSecret,
struct: AutofillStructure2,
onComplete: (suspend Dataset.Builder.() -> Unit)? = null,
provideInlinePresentation: () -> InlinePresentation?,
): Dataset? {
val title = secret.name
val text = kotlin.run {
@ -523,33 +533,29 @@ class KeyguardAutofillService : AutofillService(), DIAware {
title = title,
text = text,
)
val fields = run {
val hints = struct.items
.asSequence()
.map { it.hint }
.toSet()
secret.gett(
hints = hints,
getTotpCode = getTotpCode,
).bind()
}
val fields = DatasetBuilder.fieldsStructData(
cipher = secret,
structItems = struct.items,
getTotpCode = getTotpCode,
)
suspend fun createDatasetBuilder(): Dataset.Builder {
val builder = Dataset.Builder(views)
fun createDatasetBuilder(): Dataset.Builder {
val builder = DatasetBuilder.create(
menuPresentation = views,
fields = DatasetBuilder.fields(
structItems = struct.items,
structData = fields,
),
provideInlinePresentation = provideInlinePresentation,
)
builder.setId(secret.id)
struct.items.forEach { structItem ->
val value = fields[structItem.hint]
builder.trySetValue(
id = structItem.id,
value = value,
)
}
return builder
}
val builder = createDatasetBuilder()
try {
val dataset = createDatasetBuilder().build()
val dataset = createDatasetBuilder()
.build()
val intent = AutofillFakeAuthActivity.getIntent(
this,
dataset = dataset,
@ -557,9 +563,10 @@ class KeyguardAutofillService : AutofillService(), DIAware {
forceAddUri = false,
structure = struct,
)
val code = PendingIntents.autofill.obtainId()
val pi = PendingIntent.getActivity(
this,
10031 + index,
code,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT,
)
@ -569,8 +576,6 @@ class KeyguardAutofillService : AutofillService(), DIAware {
// Ignored
}
onComplete?.invoke(builder)
return try {
builder.build()
} catch (e: Exception) {
@ -578,18 +583,9 @@ class KeyguardAutofillService : AutofillService(), DIAware {
}
}
private fun Dataset.Builder.trySetValue(
id: AutofillId?,
value: String?,
) {
if (id != null && value != null) {
setValue(id, AutofillValue.forText(value))
}
}
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
private suspend fun tryBuildSecretInlinePresentation(
private fun tryBuildSecretInlinePresentation(
request: FillRequest,
index: Int,
secret: DSecret,
@ -602,7 +598,8 @@ class KeyguardAutofillService : AutofillService(), DIAware {
)
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
PendingIntent.getActivity(this, 1010, intent, flags)
val code = PendingIntents.autofill.obtainId()
PendingIntent.getActivity(this, code, intent, flags)
},
content = {
setContentDescription(secret.name)
@ -648,7 +645,8 @@ class KeyguardAutofillService : AutofillService(), DIAware {
index = index,
createPendingIntent = {
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
PendingIntent.getActivity(this, 1010, intent, flags)
val code = PendingIntents.autofill.obtainId()
PendingIntent.getActivity(this, code, intent, flags)
},
content = {
val title = getString(Res.string.autofill_open_keyguard)
@ -670,7 +668,8 @@ class KeyguardAutofillService : AutofillService(), DIAware {
index = index,
createPendingIntent = {
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
PendingIntent.getActivity(this, 1002, intent, flags)
val code = PendingIntents.autofill.obtainId()
PendingIntent.getActivity(this, code, intent, flags)
},
content = {
val text = when (type) {
@ -685,7 +684,7 @@ class KeyguardAutofillService : AutofillService(), DIAware {
@SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R)
private suspend inline fun tryCreateInlinePresentation(
private inline fun tryCreateInlinePresentation(
request: FillRequest,
index: Int,
createPendingIntent: () -> PendingIntent,
@ -735,7 +734,8 @@ class KeyguardAutofillService : AutofillService(), DIAware {
if (Build.VERSION.SDK_INT >= 28) {
val flags =
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
val pi = PendingIntent.getActivity(this, 10120, intent, flags)
val code = PendingIntents.autofill.obtainId()
val pi = PendingIntent.getActivity(this, code, intent, flags)
callback.onSuccess(pi.intentSender)
} else {
try {