refactor: Clean-up Android autofill service

This commit is contained in:
Artem Chepurnoy 2024-06-11 22:33:08 +03:00
parent 42e1056c69
commit 075a628499
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
3 changed files with 407 additions and 352 deletions

View File

@ -4,13 +4,11 @@ import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentSender
import android.graphics.BlendMode import android.graphics.BlendMode
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.os.CancellationSignal import android.os.CancellationSignal
import android.service.autofill.* import android.service.autofill.*
import android.util.Log
import android.view.autofill.AutofillId import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import android.widget.RemoteViews import android.widget.RemoteViews
@ -27,6 +25,8 @@ import com.artemchep.keyguard.android.MainActivity
import com.artemchep.keyguard.common.R import com.artemchep.keyguard.common.R
import com.artemchep.keyguard.common.io.* import com.artemchep.keyguard.common.io.*
import com.artemchep.keyguard.common.model.* import com.artemchep.keyguard.common.model.*
import com.artemchep.keyguard.common.service.logging.LogRepository
import com.artemchep.keyguard.common.service.logging.postDebug
import com.artemchep.keyguard.common.usecase.* import com.artemchep.keyguard.common.usecase.*
import com.artemchep.keyguard.feature.home.vault.component.FormatCardGroupLength import com.artemchep.keyguard.feature.home.vault.component.FormatCardGroupLength
import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.res.Res
@ -36,7 +36,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking import org.jetbrains.compose.resources.StringResource
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.direct import org.kodein.di.direct
@ -48,6 +48,8 @@ class KeyguardAutofillService : AutofillService(), DIAware {
private const val TAG = "AFService" private const val TAG = "AFService"
const val KEEP_CIPHERS_IN_MEMORY_FOR = 10_000L const val KEEP_CIPHERS_IN_MEMORY_FOR = 10_000L
const val SUGGESTIONS_MAX_COUNT = 10
} }
private val job = Job() private val job = Job()
@ -91,13 +93,17 @@ class KeyguardAutofillService : AutofillService(), DIAware {
.shareIn(scope, SharingStarted.WhileSubscribed(KEEP_CIPHERS_IN_MEMORY_FOR), replay = 1) .shareIn(scope, SharingStarted.WhileSubscribed(KEEP_CIPHERS_IN_MEMORY_FOR), replay = 1)
} }
private val logRepository: LogRepository by lazy {
di.direct.instance()
}
private val getTotpCode: GetTotpCode by lazy { private val getTotpCode: GetTotpCode by lazy {
di.direct.instance() di.direct.instance()
} }
private val getSuggestions by lazy { private val getSuggestions by lazy {
val model: GetSuggestions<Any?> by di.instance() val model: GetSuggestions<Any?> by di.instance()
model GetCipherSuggestions(model)
} }
private val prefInlineSuggestionsFlow by lazy { private val prefInlineSuggestionsFlow by lazy {
@ -127,97 +133,117 @@ class KeyguardAutofillService : AutofillService(), DIAware {
private val autofillStructureParser = AutofillStructureParser() private val autofillStructureParser = AutofillStructureParser()
private class GetCipherSuggestions(
private val parent: GetSuggestions<Any?>,
) : GetSuggestions<DSecret> {
override fun invoke(
ciphers: List<DSecret>,
getter: Getter<DSecret, DSecret>,
target: AutofillTarget,
): IO<List<DSecret>> = parent
.invoke(
ciphers,
Getter { it as DSecret },
target,
) as IO<List<DSecret>>
}
private class AbortAutofillException(
message: String,
) : RuntimeException(message)
@SuppressLint("NewApi") @SuppressLint("NewApi")
override fun onFillRequest( override fun onFillRequest(
request: FillRequest, request: FillRequest,
cancellationSignal: CancellationSignal, cancellationSignal: CancellationSignal,
callback: FillCallback, callback: FillCallback,
) { ) {
val autofillStructure = kotlin.runCatching { getAutofillStructureIo(request)
val structureLatest = request.fillContexts .flatMap { autofillStructure ->
if (autofillStructure.items.isEmpty()) {
throw AbortAutofillException("Nothing to autofill.")
}
getAutofillResponseIo(
request = request,
autofillStructure = autofillStructure,
)
}
.effectTap { response ->
callback.onSuccess(response)
}
.handleError { e ->
logRepository.postDebug(TAG) {
"Fill request: aborted because '$e'"
}
if (cancellationSignal.isCanceled) {
return@handleError
}
val msg = e.message ?: "Something went wrong"
callback.onFailure(msg)
}
.dispatchOn(Dispatchers.Main.immediate)
.launchIn(scope)
cancellationSignal.setOnCancelListener {
job.cancel()
}
}
private fun getAutofillStructureIo(
request: FillRequest,
) = ioEffect {
val assistStructureLatest = request.fillContexts
.map { it.structure } .map { it.structure }
.lastOrNull() .lastOrNull()
// If the structure is missing, then abort auto-filling if (assistStructureLatest == null) {
// process. throw AbortAutofillException("No structures to fill.")
?: throw IllegalStateException("No structures to fill.") }
val respectAutofillOff = prefRespectAutofillOffFlow.toIO().bindBlocking() val respectAutofillOff = prefRespectAutofillOffFlow.first()
autofillStructureParser.parse( autofillStructureParser.parse(
structureLatest, assistStructureLatest,
respectAutofillOff, respectAutofillOff,
) )
} }
// .flatMap {
// val targetApplicationId = it.applicationId private fun getSaveStructureIo(
// val keyguardApplicationId = packageName request: SaveRequest,
// if (targetApplicationId == keyguardApplicationId) { ) = ioEffect {
// val response = FillResponse.Builder() val assistStructureLatest = request.fillContexts
// .disableAutofill(1000L) .map { it.structure }
// .build() .lastOrNull()
// callback.onSuccess(response) if (assistStructureLatest == null) {
// return throw AbortAutofillException("No structures to save.")
// } else {
// Result.success(it)
// }
// }
.fold(
onSuccess = ::identity,
onFailure = {
callback.onFailure("Failed to parse structures: ${it.message}")
return
},
)
if (autofillStructure.items.isEmpty()) {
callback.onFailure("Nothing to autofill.")
return
} }
val job = ciphersFlow val respectAutofillOff = prefRespectAutofillOffFlow.first()
.onStart { autofillStructureParser.parse(
Log.e("LOL", "on start v2") assistStructureLatest,
respectAutofillOff,
)
} }
private fun getAutofillResponseIo(
request: FillRequest,
autofillStructure: AutofillStructure2,
) = ciphersFlow
.toIO() .toIO()
.effectMap(Dispatchers.Default) { state -> .effectMap(Dispatchers.Default) { state ->
val autofillTarget = autofillStructure.toAutofillTarget()
state.map { secrets -> state.map { secrets ->
val autofillTarget = AutofillTarget(
links = listOfNotNull(
// application id
autofillStructure.applicationId?.let {
LinkInfoPlatform.Android(
packageName = it,
)
},
// website
autofillStructure.webDomain?.let {
val url = Url("https://$it")
LinkInfoPlatform.Web(
url = url,
frontPageUrl = url,
)
},
),
hints = autofillStructure.items.map { it.hint },
)
getSuggestions( getSuggestions(
secrets, secrets,
Getter { it as DSecret }, Getter { it },
autofillTarget, autofillTarget,
).bind() ).bind().take(SUGGESTIONS_MAX_COUNT)
.take(10) as List<DSecret>
} }
} }
.biEffectTap( .effectMap { r ->
ifException = { val shouldInlineSuggestions = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
// If the request is canceled, then it does not expect any
// feedback.
if (!cancellationSignal.isCanceled) {
callback.onFailure("Failed to get the secrets from the database!")
}
},
ifSuccess = { r ->
val canInlineSuggestions = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
prefInlineSuggestionsFlow.toIO() prefInlineSuggestionsFlow.toIO()
.attempt().bind().exists { it } .attempt().bind().isRight { it }
val forceHideManualSelection = kotlin.run { val forceHideManualSelection = kotlin.run {
val targetApplicationId = autofillStructure.applicationId val targetApplicationId = autofillStructure.applicationId
@ -230,30 +256,28 @@ class KeyguardAutofillService : AutofillService(), DIAware {
when (r) { when (r) {
is Either.Left -> { is Either.Left -> {
if (forceHideManualSelection) { if (forceHideManualSelection) {
callback.onFailure("Can not autofill own app password.") throw AbortAutofillException("Can not autofill own app password.")
return@biEffectTap
} }
// Database is locked, create a generic // Database is locked, create a generic
// sign in with option. // sign in with option.
responseBuilder.buildAuthentication( responseBuilder.buildAuthentication(
type = FooBar.UNLOCK, type = AuthenticationType.UNLOCK,
result2 = autofillStructure, struct = autofillStructure,
request = request, request = request,
canInlineSuggestions = canInlineSuggestions, canInlineSuggestions = shouldInlineSuggestions,
) )
} }
is Either.Right -> if (r.value.isEmpty()) { is Either.Right -> if (r.value.isEmpty()) {
if (forceHideManualSelection) { if (forceHideManualSelection) {
callback.onFailure("No match found.") throw AbortAutofillException("No match found.")
return@biEffectTap
} }
// No match found, create a generic option. // No match found, create a generic option.
responseBuilder.buildAuthentication( responseBuilder.buildAuthentication(
type = FooBar.SELECT, type = AuthenticationType.SELECT,
result2 = autofillStructure, struct = autofillStructure,
request = request, request = request,
canInlineSuggestions = canInlineSuggestions, canInlineSuggestions = shouldInlineSuggestions,
) )
} else { } else {
val manualSelection = prefManualSelectionFlow.toIO().bind() && val manualSelection = prefManualSelectionFlow.toIO().bind() &&
@ -261,7 +285,7 @@ class KeyguardAutofillService : AutofillService(), DIAware {
val totalInlineSuggestionsMaxCount = if ( val totalInlineSuggestionsMaxCount = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
canInlineSuggestions shouldInlineSuggestions
) { ) {
request.inlineSuggestionsRequest?.maxSuggestionCount request.inlineSuggestionsRequest?.maxSuggestionCount
?: 0 // no suggestions allowed ?: 0 // no suggestions allowed
@ -273,23 +297,23 @@ class KeyguardAutofillService : AutofillService(), DIAware {
var index = 0 var index = 0
r.value.forEach { secret -> r.value.forEach { secret ->
var f = false var datasetHasInlinePresentation = false
val dataset = val dataset =
tryBuildDataset(index, this, secret, autofillStructure) { tryBuildDataset(index, this, secret, autofillStructure) {
if (index < secretInlineSuggestionsMaxCount) { if (index < secretInlineSuggestionsMaxCount) {
val inline = val inlinePresentation =
tryBuildSecretInlinePresentation( tryBuildSecretInlinePresentation(
request, request,
index, index,
secret, secret,
) )
if (inline != null) { if (inlinePresentation != null) {
setInlinePresentation(inline) setInlinePresentation(inlinePresentation)
f = true datasetHasInlinePresentation = true
} }
} }
} }
if (dataset != null && (!canInlineSuggestions || f)) { if (dataset != null && (!shouldInlineSuggestions || datasetHasInlinePresentation)) {
responseBuilder.addDataset(dataset) responseBuilder.addDataset(dataset)
index += 1 index += 1
} }
@ -314,10 +338,9 @@ class KeyguardAutofillService : AutofillService(), DIAware {
autofillStructure.items.forEach { autofillStructure.items.forEach {
val autofillId = it.id val autofillId = it.id
Log.e("SuggestionsTest", "autofill_id=$autofillId")
val builder = Dataset.Builder(manualSelectionView) val builder = Dataset.Builder(manualSelectionView)
if (totalInlineSuggestionsMaxCount > 0 && canInlineSuggestions && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (totalInlineSuggestionsMaxCount > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlinePresentation = val inlinePresentation =
tryBuildManualSelectionInlinePresentation( tryBuildManualSelectionInlinePresentation(
request, request,
@ -327,10 +350,6 @@ class KeyguardAutofillService : AutofillService(), DIAware {
inlinePresentation?.let { inlinePresentation?.let {
builder.setInlinePresentation(it) builder.setInlinePresentation(it)
} }
Log.e(
"SuggestionsTest",
"adding inline=$inlinePresentation",
)
} }
builder.setValue(autofillId, null) builder.setValue(autofillId, null)
builder.setAuthentication(pi.intentSender) builder.setAuthentication(pi.intentSender)
@ -347,7 +366,7 @@ class KeyguardAutofillService : AutofillService(), DIAware {
val item: AutofillStructure2.Item, val item: AutofillStructure2.Item,
) )
val items = mutableListOf<SaveItem>() val saveItems = mutableListOf<SaveItem>()
autofillStructure.items autofillStructure.items
.distinctBy { it.hint } .distinctBy { it.hint }
.forEach { item -> .forEach { item ->
@ -357,7 +376,7 @@ class KeyguardAutofillService : AutofillService(), DIAware {
AutofillHint.USERNAME -> SaveInfo.SAVE_DATA_TYPE_USERNAME AutofillHint.USERNAME -> SaveInfo.SAVE_DATA_TYPE_USERNAME
else -> return@forEach else -> return@forEach
} }
items += SaveItem( saveItems += SaveItem(
flag = flag, flag = flag,
item = item, item = item,
) )
@ -377,11 +396,11 @@ class KeyguardAutofillService : AutofillService(), DIAware {
if ( if (
hintsIncludeUsername && hintsIncludeUsername &&
hintsIncludePassword && hintsIncludePassword &&
items.isNotEmpty() saveItems.isNotEmpty()
) { ) {
val saveInfoBuilder = SaveInfo.Builder( val saveInfoBuilder = SaveInfo.Builder(
items.fold(0) { y, x -> y or x.flag }, saveItems.fold(0) { y, x -> y or x.flag },
items saveItems
.map { it.item.id } .map { it.item.id }
.toTypedArray(), .toTypedArray(),
) )
@ -389,66 +408,76 @@ class KeyguardAutofillService : AutofillService(), DIAware {
responseBuilder.setSaveInfo(saveInfo) responseBuilder.setSaveInfo(saveInfo)
} }
} }
try { responseBuilder
val response = responseBuilder
.build() .build()
callback.onSuccess(response)
} catch (e: Exception) {
callback.onFailure("Failed to build response ${e.localizedMessage}")
}
},
)
.dispatchOn(Dispatchers.Main)
.launchIn(scope)
cancellationSignal.setOnCancelListener {
job.cancel()
}
} }
private enum class FooBar { private fun AutofillStructure2.toAutofillTarget(
) = AutofillTarget(
links = listOfNotNull(
// application id
applicationId?.let {
LinkInfoPlatform.Android(
packageName = it,
)
},
// website
webDomain?.let {
val schema = webScheme ?: "https"
val url = Url("$schema://$it")
LinkInfoPlatform.Web(
url = url,
frontPageUrl = url,
)
},
),
hints = items.map { it.hint },
maxCount = SUGGESTIONS_MAX_COUNT,
)
private enum class AuthenticationType {
UNLOCK, UNLOCK,
SELECT, SELECT,
} }
private suspend fun FillResponse.Builder.buildAuthentication( private suspend fun FillResponse.Builder.buildAuthentication(
type: FooBar, type: AuthenticationType,
result2: AutofillStructure2, struct: AutofillStructure2,
request: FillRequest, request: FillRequest,
canInlineSuggestions: Boolean, canInlineSuggestions: Boolean,
) { ) {
val remoteViewsUnlock: RemoteViews = when (type) { val remoteViews: RemoteViews = when (type) {
FooBar.UNLOCK -> AutofillViews.buildPopupKeyguardUnlock( AuthenticationType.UNLOCK -> AutofillViews.buildPopupKeyguardUnlock(
this@KeyguardAutofillService, this@KeyguardAutofillService,
result2.webDomain, struct.webDomain,
result2.applicationId, struct.applicationId,
) )
FooBar.SELECT -> AutofillViews.buildPopupKeyguardOpen( AuthenticationType.SELECT -> AutofillViews.buildPopupKeyguardOpen(
this@KeyguardAutofillService, this@KeyguardAutofillService,
result2.webDomain, struct.webDomain,
result2.applicationId, struct.applicationId,
) )
} }
result2.items.forEach {
val autofillId = it.id
val authIntent = AutofillActivity.getIntent( val authIntent = AutofillActivity.getIntent(
context = this@KeyguardAutofillService, context = this@KeyguardAutofillService,
args = AutofillActivity.Args( args = AutofillActivity.Args(
applicationId = result2.applicationId, applicationId = struct.applicationId,
webDomain = result2.webDomain, webDomain = struct.webDomain,
webScheme = result2.webScheme, webScheme = struct.webScheme,
autofillStructure2 = result2, autofillStructure2 = struct,
), ),
) )
val intentSender: IntentSender = PendingIntent.getActivity( val authIntentSender = PendingIntent.getActivity(
this@KeyguardAutofillService, this@KeyguardAutofillService,
1001, 1001,
authIntent, authIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
).intentSender ).intentSender
val builder = Dataset.Builder(remoteViewsUnlock) struct.items.forEach { item ->
val builder = Dataset.Builder(remoteViews)
if (canInlineSuggestions && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (canInlineSuggestions && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlinePresentation = val inlinePresentation =
tryCreateAuthenticationInlinePresentation( tryCreateAuthenticationInlinePresentation(
@ -460,13 +489,9 @@ class KeyguardAutofillService : AutofillService(), DIAware {
inlinePresentation?.let { inlinePresentation?.let {
builder.setInlinePresentation(it) builder.setInlinePresentation(it)
} }
Log.e(
"SuggestionsTest",
"adding inline=$inlinePresentation",
)
} }
builder.setValue(autofillId, null) builder.setValue(item.id, null)
builder.setAuthentication(intentSender) builder.setAuthentication(authIntentSender)
addDataset(builder.build()) addDataset(builder.build())
} }
} }
@ -620,7 +645,7 @@ class KeyguardAutofillService : AutofillService(), DIAware {
PendingIntent.getActivity(this, 1010, intent, flags) PendingIntent.getActivity(this, 1010, intent, flags)
}, },
content = { content = {
val title = org.jetbrains.compose.resources.getString(Res.string.autofill_open_keyguard) val title = getString(Res.string.autofill_open_keyguard)
setContentDescription(title) setContentDescription(title)
setTitle(title) setTitle(title)
setStartIcon(createAppIcon()) setStartIcon(createAppIcon())
@ -630,7 +655,7 @@ class KeyguardAutofillService : AutofillService(), DIAware {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
private suspend fun tryCreateAuthenticationInlinePresentation( private suspend fun tryCreateAuthenticationInlinePresentation(
type: FooBar, type: AuthenticationType,
request: FillRequest, request: FillRequest,
index: Int, index: Int,
intent: Intent, intent: Intent,
@ -643,8 +668,8 @@ class KeyguardAutofillService : AutofillService(), DIAware {
}, },
content = { content = {
val text = when (type) { val text = when (type) {
FooBar.UNLOCK -> org.jetbrains.compose.resources.getString(Res.string.autofill_unlock_keyguard) AuthenticationType.UNLOCK -> getString(Res.string.autofill_unlock_keyguard)
FooBar.SELECT -> org.jetbrains.compose.resources.getString(Res.string.autofill_open_keyguard) AuthenticationType.SELECT -> getString(Res.string.autofill_open_keyguard)
} }
setContentDescription(text) setContentDescription(text)
setTitle(text) setTitle(text)
@ -689,26 +714,49 @@ class KeyguardAutofillService : AutofillService(), DIAware {
} }
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
val autofillStructure = kotlin.runCatching { getSaveStructureIo(request)
val structureLatest = request.fillContexts .flatMap { autofillStructure ->
.map { it.structure } if (autofillStructure.items.isEmpty()) {
.lastOrNull() throw AbortAutofillException("Nothing to autofill.")
// If the structure is missing, then abort auto-filling }
// process.
?: throw IllegalStateException("No structures to fill.")
val respectAutofillOff = prefRespectAutofillOffFlow.toIO().bindBlocking() getSaveResponseIo(
autofillStructureParser.parse( request = request,
structureLatest, autofillStructure = autofillStructure,
respectAutofillOff,
)
}.fold(
onSuccess = ::identity,
onFailure = {
callback.onFailure("Failed to parse structures: ${it.message}")
return
},
) )
}
.effectTap { intent ->
if (Build.VERSION.SDK_INT >= 28) {
val flags =
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
val pi = PendingIntent.getActivity(this, 10120, intent, flags)
callback.onSuccess(pi.intentSender)
} else {
try {
startActivity(intent)
} catch (e: Exception) {
callback.onFailure(e.message)
return@effectTap
}
callback.onSuccess()
}
}
.handleError { e ->
logRepository.postDebug(TAG) {
"Save request: aborted because '$e'"
}
val msg = e.message ?: "Something went wrong"
callback.onFailure(msg)
}
.dispatchOn(Dispatchers.Main.immediate)
.launchIn(scope)
}
private fun getSaveResponseIo(
request: SaveRequest,
autofillStructure: AutofillStructure2,
) = ioEffect {
val hints = autofillStructure val hints = autofillStructure
.items .items
.asSequence() .asSequence()
@ -724,11 +772,10 @@ class KeyguardAutofillService : AutofillService(), DIAware {
!hintsIncludeUsername || !hintsIncludeUsername ||
!hintsIncludePassword !hintsIncludePassword
) { ) {
callback.onFailure("Can only save login data.") throw AbortAutofillException("Can only save login data.")
return
} }
val intent = AutofillSaveActivity.getIntent( AutofillSaveActivity.getIntent(
context = this@KeyguardAutofillService, context = this@KeyguardAutofillService,
args = AutofillSaveActivity.Args( args = AutofillSaveActivity.Args(
applicationId = autofillStructure.applicationId, applicationId = autofillStructure.applicationId,
@ -737,24 +784,13 @@ class KeyguardAutofillService : AutofillService(), DIAware {
autofillStructure2 = autofillStructure, autofillStructure2 = autofillStructure,
), ),
) )
if (Build.VERSION.SDK_INT >= 28) {
val flags =
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
val pi = PendingIntent.getActivity(this, 10120, intent, flags)
callback.onSuccess(pi.intentSender)
} else {
try {
startActivity(intent)
} catch (e: Exception) {
callback.onFailure(e.message)
return
}
callback.onSuccess()
}
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
job.cancel() job.cancel()
} }
private suspend fun getString(res: StringResource) =
org.jetbrains.compose.resources.getString(res)
} }

View File

@ -3,4 +3,5 @@ package com.artemchep.keyguard.common.model
data class AutofillTarget( data class AutofillTarget(
val links: List<LinkInfoPlatform>, val links: List<LinkInfoPlatform>,
val hints: List<AutofillHint>, val hints: List<AutofillHint>,
val maxCount: Int = -1,
) )

View File

@ -477,17 +477,28 @@ fun DSecret.contains(hint: AutofillHint) = when (hint) {
AutofillHint.POSTAL_ADDRESS_EXTENDED_POSTAL_CODE -> false // not supported AutofillHint.POSTAL_ADDRESS_EXTENDED_POSTAL_CODE -> false // not supported
AutofillHint.POSTAL_ADDRESS_APT_NUMBER -> identity?.address1 != null AutofillHint.POSTAL_ADDRESS_APT_NUMBER -> identity?.address1 != null
AutofillHint.POSTAL_ADDRESS_DEPENDENT_LOCALITY -> false // not supported AutofillHint.POSTAL_ADDRESS_DEPENDENT_LOCALITY -> false // not supported
AutofillHint.PERSON_NAME -> identity?.firstName != null AutofillHint.PERSON_NAME -> identity?.firstName != null ||
AutofillHint.PERSON_NAME_GIVEN -> false // not supported identity?.middleName != null ||
identity?.lastName != null
AutofillHint.PERSON_NAME_GIVEN -> identity?.firstName != null
AutofillHint.PERSON_NAME_FAMILY -> identity?.lastName != null AutofillHint.PERSON_NAME_FAMILY -> identity?.lastName != null
AutofillHint.PERSON_NAME_MIDDLE -> identity?.middleName != null AutofillHint.PERSON_NAME_MIDDLE -> identity?.middleName != null
AutofillHint.PERSON_NAME_MIDDLE_INITIAL -> identity?.middleName != null AutofillHint.PERSON_NAME_MIDDLE_INITIAL -> identity?.middleName != null
AutofillHint.PERSON_NAME_PREFIX -> false // not supported AutofillHint.PERSON_NAME_PREFIX -> false // not supported
AutofillHint.PERSON_NAME_SUFFIX -> false // not supported AutofillHint.PERSON_NAME_SUFFIX -> false // not supported
AutofillHint.PHONE_NUMBER -> identity?.phone != null AutofillHint.PHONE_NUMBER -> identity?.phone != null ||
AutofillHint.PHONE_NUMBER_DEVICE -> false // not supported login?.username?.takeIf { REGEX_PHONE_NUMBER.matches(it) } != null
AutofillHint.PHONE_COUNTRY_CODE -> identity?.phone != null // TODO: Extract country code
AutofillHint.PHONE_NATIONAL -> false // not supported AutofillHint.PHONE_NUMBER_DEVICE -> identity?.phone != null ||
login?.username?.takeIf { REGEX_PHONE_NUMBER.matches(it) } != null
AutofillHint.PHONE_COUNTRY_CODE -> identity?.phone != null ||
login?.username?.takeIf { REGEX_PHONE_NUMBER.matches(it) } != null
AutofillHint.PHONE_NATIONAL -> identity?.phone != null ||
login?.username?.takeIf { REGEX_PHONE_NUMBER.matches(it) } != null
AutofillHint.NEW_USERNAME -> false // not supported AutofillHint.NEW_USERNAME -> false // not supported
AutofillHint.NEW_PASSWORD -> false // not supported AutofillHint.NEW_PASSWORD -> false // not supported
AutofillHint.GENDER -> false // not supported AutofillHint.GENDER -> false // not supported
@ -529,8 +540,13 @@ fun DSecret.get(
AutofillHint.POSTAL_ADDRESS_EXTENDED_POSTAL_CODE -> null AutofillHint.POSTAL_ADDRESS_EXTENDED_POSTAL_CODE -> null
AutofillHint.POSTAL_ADDRESS_APT_NUMBER -> identity?.address1 AutofillHint.POSTAL_ADDRESS_APT_NUMBER -> identity?.address1
AutofillHint.POSTAL_ADDRESS_DEPENDENT_LOCALITY -> null AutofillHint.POSTAL_ADDRESS_DEPENDENT_LOCALITY -> null
AutofillHint.PERSON_NAME -> identity?.firstName AutofillHint.PERSON_NAME -> listOfNotNull(
AutofillHint.PERSON_NAME_GIVEN -> null identity?.firstName,
identity?.middleName,
identity?.lastName,
).joinToString(separator = " ").takeIf { it.isNotEmpty() }
AutofillHint.PERSON_NAME_GIVEN -> identity?.firstName
AutofillHint.PERSON_NAME_FAMILY -> identity?.lastName AutofillHint.PERSON_NAME_FAMILY -> identity?.lastName
AutofillHint.PERSON_NAME_MIDDLE -> identity?.middleName AutofillHint.PERSON_NAME_MIDDLE -> identity?.middleName
AutofillHint.PERSON_NAME_MIDDLE_INITIAL -> identity?.middleName AutofillHint.PERSON_NAME_MIDDLE_INITIAL -> identity?.middleName
@ -541,7 +557,9 @@ fun DSecret.get(
AutofillHint.PHONE_NUMBER_DEVICE -> null AutofillHint.PHONE_NUMBER_DEVICE -> null
AutofillHint.PHONE_COUNTRY_CODE -> identity?.phone // TODO: Extract country code AutofillHint.PHONE_COUNTRY_CODE -> identity?.phone // TODO: Extract country code
AutofillHint.PHONE_NATIONAL -> null AutofillHint.PHONE_NATIONAL -> identity?.phone
?: login?.username?.takeIf { REGEX_PHONE_NUMBER.matches(it) }
AutofillHint.NEW_USERNAME -> null AutofillHint.NEW_USERNAME -> null
AutofillHint.NEW_PASSWORD -> null AutofillHint.NEW_PASSWORD -> null
AutofillHint.GENDER -> null AutofillHint.GENDER -> null