mirror of
https://github.com/AChep/keyguard-app.git
synced 2025-01-04 18:59:15 +01:00
refactor: Use modern Autofill dataset builder on newer Android versions
This commit is contained in:
parent
5d20685465
commit
9fa53ea157
@ -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,
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user