refactor: Clean-up Android autofill service
This commit is contained in:
parent
42e1056c69
commit
075a628499
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue