diff --git a/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardAutofillService.kt b/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardAutofillService.kt index 7ba0bad..33d3a91 100644 --- a/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardAutofillService.kt +++ b/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardAutofillService.kt @@ -4,13 +4,11 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.IntentSender import android.graphics.BlendMode import android.graphics.drawable.Icon import android.os.Build import android.os.CancellationSignal import android.service.autofill.* -import android.util.Log import android.view.autofill.AutofillId import android.view.autofill.AutofillValue 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.io.* 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.feature.home.vault.component.FormatCardGroupLength import com.artemchep.keyguard.res.Res @@ -36,7 +36,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* -import kotlinx.coroutines.runBlocking +import org.jetbrains.compose.resources.StringResource import org.kodein.di.DIAware import org.kodein.di.android.closestDI import org.kodein.di.direct @@ -48,6 +48,8 @@ class KeyguardAutofillService : AutofillService(), DIAware { private const val TAG = "AFService" const val KEEP_CIPHERS_IN_MEMORY_FOR = 10_000L + + const val SUGGESTIONS_MAX_COUNT = 10 } private val job = Job() @@ -91,13 +93,17 @@ class KeyguardAutofillService : AutofillService(), DIAware { .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 { di.direct.instance() } private val getSuggestions by lazy { val model: GetSuggestions by di.instance() - model + GetCipherSuggestions(model) } private val prefInlineSuggestionsFlow by lazy { @@ -127,328 +133,351 @@ class KeyguardAutofillService : AutofillService(), DIAware { private val autofillStructureParser = AutofillStructureParser() + private class GetCipherSuggestions( + private val parent: GetSuggestions, + ) : GetSuggestions { + override fun invoke( + ciphers: List, + getter: Getter, + target: AutofillTarget, + ): IO> = parent + .invoke( + ciphers, + Getter { it as DSecret }, + target, + ) as IO> + } + + private class AbortAutofillException( + message: String, + ) : RuntimeException(message) + @SuppressLint("NewApi") override fun onFillRequest( request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback, ) { - val autofillStructure = kotlin.runCatching { - val structureLatest = request.fillContexts - .map { it.structure } - .lastOrNull() - // If the structure is missing, then abort auto-filling - // process. - ?: throw IllegalStateException("No structures to fill.") - - val respectAutofillOff = prefRespectAutofillOffFlow.toIO().bindBlocking() - autofillStructureParser.parse( - structureLatest, - respectAutofillOff, - ) - } -// .flatMap { -// val targetApplicationId = it.applicationId -// val keyguardApplicationId = packageName -// if (targetApplicationId == keyguardApplicationId) { -// val response = FillResponse.Builder() -// .disableAutofill(1000L) -// .build() -// callback.onSuccess(response) -// return -// } 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 - .onStart { - Log.e("LOL", "on start v2") - } - .toIO() - .effectMap(Dispatchers.Default) { state -> - 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( - secrets, - Getter { it as DSecret }, - autofillTarget, - ).bind() - .take(10) as List + getAutofillStructureIo(request) + .flatMap { autofillStructure -> + if (autofillStructure.items.isEmpty()) { + throw AbortAutofillException("Nothing to autofill.") } + + getAutofillResponseIo( + request = request, + autofillStructure = autofillStructure, + ) } - .biEffectTap( - ifException = { - // 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() - .attempt().bind().exists { it } + .effectTap { response -> + callback.onSuccess(response) + } + .handleError { e -> + logRepository.postDebug(TAG) { + "Fill request: aborted because '$e'" + } - val forceHideManualSelection = kotlin.run { - val targetApplicationId = autofillStructure.applicationId - val keyguardApplicationId = packageName - targetApplicationId == keyguardApplicationId - } + if (cancellationSignal.isCanceled) { + return@handleError + } - // build a response - val responseBuilder = FillResponse.Builder() - when (r) { - is Either.Left -> { - if (forceHideManualSelection) { - callback.onFailure("Can not autofill own app password.") - return@biEffectTap - } - // Database is locked, create a generic - // sign in with option. - responseBuilder.buildAuthentication( - type = FooBar.UNLOCK, - result2 = autofillStructure, - request = request, - canInlineSuggestions = canInlineSuggestions, - ) - } - - is Either.Right -> if (r.value.isEmpty()) { - if (forceHideManualSelection) { - callback.onFailure("No match found.") - return@biEffectTap - } - // No match found, create a generic option. - responseBuilder.buildAuthentication( - type = FooBar.SELECT, - result2 = autofillStructure, - request = request, - canInlineSuggestions = canInlineSuggestions, - ) - } else { - val manualSelection = prefManualSelectionFlow.toIO().bind() && - !forceHideManualSelection - - val totalInlineSuggestionsMaxCount = if ( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && - canInlineSuggestions - ) { - request.inlineSuggestionsRequest?.maxSuggestionCount - ?: 0 // no suggestions allowed - } else { - 0 - } - val secretInlineSuggestionsMaxCount = - if (manualSelection) totalInlineSuggestionsMaxCount - 1 else totalInlineSuggestionsMaxCount - - var index = 0 - r.value.forEach { secret -> - var f = false - val dataset = - tryBuildDataset(index, this, secret, autofillStructure) { - if (index < secretInlineSuggestionsMaxCount) { - val inline = - tryBuildSecretInlinePresentation( - request, - index, - secret, - ) - if (inline != null) { - setInlinePresentation(inline) - f = true - } - } - } - if (dataset != null && (!canInlineSuggestions || f)) { - responseBuilder.addDataset(dataset) - index += 1 - } - } - if (manualSelection) { - val intent = AutofillActivity.getIntent( - context = this@KeyguardAutofillService, - args = AutofillActivity.Args( - applicationId = autofillStructure.applicationId, - webDomain = autofillStructure.webDomain, - webScheme = autofillStructure.webScheme, - autofillStructure2 = autofillStructure, - ), - ) - - val flags = - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - val pi = PendingIntent.getActivity(this, 1010, intent, flags) - - val manualSelectionView = AutofillViews - .buildPopupEntryManual(this) - - autofillStructure.items.forEach { - val autofillId = it.id - Log.e("SuggestionsTest", "autofill_id=$autofillId") - val builder = Dataset.Builder(manualSelectionView) - - if (totalInlineSuggestionsMaxCount > 0 && canInlineSuggestions && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val inlinePresentation = - tryBuildManualSelectionInlinePresentation( - request, - index, - intent = intent, - ) - inlinePresentation?.let { - builder.setInlinePresentation(it) - } - Log.e( - "SuggestionsTest", - "adding inline=$inlinePresentation", - ) - } - builder.setValue(autofillId, null) - builder.setAuthentication(pi.intentSender) - responseBuilder.addDataset(builder.build()) - } - } - } - } - val shouldSaveRequest = prefSaveRequestFlow.first() && - !forceHideManualSelection - if (shouldSaveRequest) { - class SaveItem( - val flag: Int, - val item: AutofillStructure2.Item, - ) - - val items = mutableListOf() - autofillStructure.items - .distinctBy { it.hint } - .forEach { item -> - val flag = when (item.hint) { - AutofillHint.PASSWORD -> SaveInfo.SAVE_DATA_TYPE_PASSWORD - AutofillHint.EMAIL_ADDRESS -> SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS - AutofillHint.USERNAME -> SaveInfo.SAVE_DATA_TYPE_USERNAME - else -> return@forEach - } - items += SaveItem( - flag = flag, - item = item, - ) - } - - val hints = autofillStructure - .items - .asSequence() - .map { it.hint } - .toSet() - val hintsIncludeUsername = AutofillHint.USERNAME in hints || - AutofillHint.NEW_USERNAME in hints || - AutofillHint.PHONE_NUMBER in hints || - AutofillHint.EMAIL_ADDRESS in hints - val hintsIncludePassword = AutofillHint.PASSWORD in hints || - AutofillHint.NEW_PASSWORD in hints - if ( - hintsIncludeUsername && - hintsIncludePassword && - items.isNotEmpty() - ) { - val saveInfoBuilder = SaveInfo.Builder( - items.fold(0) { y, x -> y or x.flag }, - items - .map { it.item.id } - .toTypedArray(), - ) - val saveInfo = saveInfoBuilder.build() - responseBuilder.setSaveInfo(saveInfo) - } - } - try { - val response = responseBuilder - .build() - callback.onSuccess(response) - } catch (e: Exception) { - callback.onFailure("Failed to build response ${e.localizedMessage}") - } - }, - ) - .dispatchOn(Dispatchers.Main) + val msg = e.message ?: "Something went wrong" + callback.onFailure(msg) + } + .dispatchOn(Dispatchers.Main.immediate) .launchIn(scope) cancellationSignal.setOnCancelListener { job.cancel() } } - private enum class FooBar { + private fun getAutofillStructureIo( + request: FillRequest, + ) = ioEffect { + val assistStructureLatest = request.fillContexts + .map { it.structure } + .lastOrNull() + if (assistStructureLatest == null) { + throw AbortAutofillException("No structures to fill.") + } + + val respectAutofillOff = prefRespectAutofillOffFlow.first() + autofillStructureParser.parse( + assistStructureLatest, + respectAutofillOff, + ) + } + + private fun getSaveStructureIo( + request: SaveRequest, + ) = ioEffect { + val assistStructureLatest = request.fillContexts + .map { it.structure } + .lastOrNull() + if (assistStructureLatest == null) { + throw AbortAutofillException("No structures to save.") + } + + val respectAutofillOff = prefRespectAutofillOffFlow.first() + autofillStructureParser.parse( + assistStructureLatest, + respectAutofillOff, + ) + } + + private fun getAutofillResponseIo( + request: FillRequest, + autofillStructure: AutofillStructure2, + ) = ciphersFlow + .toIO() + .effectMap(Dispatchers.Default) { state -> + val autofillTarget = autofillStructure.toAutofillTarget() + state.map { secrets -> + getSuggestions( + secrets, + Getter { it }, + autofillTarget, + ).bind().take(SUGGESTIONS_MAX_COUNT) + } + } + .effectMap { r -> + val shouldInlineSuggestions = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + prefInlineSuggestionsFlow.toIO() + .attempt().bind().isRight { it } + + val forceHideManualSelection = kotlin.run { + val targetApplicationId = autofillStructure.applicationId + val keyguardApplicationId = packageName + targetApplicationId == keyguardApplicationId + } + + // build a response + val responseBuilder = FillResponse.Builder() + when (r) { + is Either.Left -> { + if (forceHideManualSelection) { + throw AbortAutofillException("Can not autofill own app password.") + } + // Database is locked, create a generic + // sign in with option. + responseBuilder.buildAuthentication( + type = AuthenticationType.UNLOCK, + struct = autofillStructure, + request = request, + canInlineSuggestions = shouldInlineSuggestions, + ) + } + + is Either.Right -> if (r.value.isEmpty()) { + if (forceHideManualSelection) { + throw AbortAutofillException("No match found.") + } + // No match found, create a generic option. + responseBuilder.buildAuthentication( + type = AuthenticationType.SELECT, + struct = autofillStructure, + request = request, + canInlineSuggestions = shouldInlineSuggestions, + ) + } else { + val manualSelection = prefManualSelectionFlow.toIO().bind() && + !forceHideManualSelection + + val totalInlineSuggestionsMaxCount = if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + shouldInlineSuggestions + ) { + request.inlineSuggestionsRequest?.maxSuggestionCount + ?: 0 // no suggestions allowed + } else { + 0 + } + val secretInlineSuggestionsMaxCount = + if (manualSelection) totalInlineSuggestionsMaxCount - 1 else totalInlineSuggestionsMaxCount + + var index = 0 + r.value.forEach { secret -> + var datasetHasInlinePresentation = false + val dataset = + tryBuildDataset(index, this, secret, autofillStructure) { + if (index < secretInlineSuggestionsMaxCount) { + val inlinePresentation = + tryBuildSecretInlinePresentation( + request, + index, + secret, + ) + if (inlinePresentation != null) { + setInlinePresentation(inlinePresentation) + datasetHasInlinePresentation = true + } + } + } + if (dataset != null && (!shouldInlineSuggestions || datasetHasInlinePresentation)) { + responseBuilder.addDataset(dataset) + index += 1 + } + } + if (manualSelection) { + val intent = AutofillActivity.getIntent( + context = this@KeyguardAutofillService, + args = AutofillActivity.Args( + applicationId = autofillStructure.applicationId, + webDomain = autofillStructure.webDomain, + webScheme = autofillStructure.webScheme, + autofillStructure2 = autofillStructure, + ), + ) + + val flags = + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + val pi = PendingIntent.getActivity(this, 1010, intent, flags) + + val manualSelectionView = AutofillViews + .buildPopupEntryManual(this) + + autofillStructure.items.forEach { + val autofillId = it.id + val builder = Dataset.Builder(manualSelectionView) + + 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()) + } + } + } + } + val shouldSaveRequest = prefSaveRequestFlow.first() && + !forceHideManualSelection + if (shouldSaveRequest) { + class SaveItem( + val flag: Int, + val item: AutofillStructure2.Item, + ) + + val saveItems = mutableListOf() + autofillStructure.items + .distinctBy { it.hint } + .forEach { item -> + val flag = when (item.hint) { + AutofillHint.PASSWORD -> SaveInfo.SAVE_DATA_TYPE_PASSWORD + AutofillHint.EMAIL_ADDRESS -> SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS + AutofillHint.USERNAME -> SaveInfo.SAVE_DATA_TYPE_USERNAME + else -> return@forEach + } + saveItems += SaveItem( + flag = flag, + item = item, + ) + } + + val hints = autofillStructure + .items + .asSequence() + .map { it.hint } + .toSet() + val hintsIncludeUsername = AutofillHint.USERNAME in hints || + AutofillHint.NEW_USERNAME in hints || + AutofillHint.PHONE_NUMBER in hints || + AutofillHint.EMAIL_ADDRESS in hints + val hintsIncludePassword = AutofillHint.PASSWORD in hints || + AutofillHint.NEW_PASSWORD in hints + if ( + hintsIncludeUsername && + hintsIncludePassword && + saveItems.isNotEmpty() + ) { + val saveInfoBuilder = SaveInfo.Builder( + saveItems.fold(0) { y, x -> y or x.flag }, + saveItems + .map { it.item.id } + .toTypedArray(), + ) + val saveInfo = saveInfoBuilder.build() + responseBuilder.setSaveInfo(saveInfo) + } + } + responseBuilder + .build() + } + + 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, SELECT, } private suspend fun FillResponse.Builder.buildAuthentication( - type: FooBar, - result2: AutofillStructure2, + type: AuthenticationType, + struct: AutofillStructure2, request: FillRequest, canInlineSuggestions: Boolean, ) { - val remoteViewsUnlock: RemoteViews = when (type) { - FooBar.UNLOCK -> AutofillViews.buildPopupKeyguardUnlock( + val remoteViews: RemoteViews = when (type) { + AuthenticationType.UNLOCK -> AutofillViews.buildPopupKeyguardUnlock( this@KeyguardAutofillService, - result2.webDomain, - result2.applicationId, + struct.webDomain, + struct.applicationId, ) - FooBar.SELECT -> AutofillViews.buildPopupKeyguardOpen( + AuthenticationType.SELECT -> AutofillViews.buildPopupKeyguardOpen( this@KeyguardAutofillService, - result2.webDomain, - result2.applicationId, + struct.webDomain, + struct.applicationId, ) } - result2.items.forEach { - val autofillId = it.id - val authIntent = AutofillActivity.getIntent( - context = this@KeyguardAutofillService, - args = AutofillActivity.Args( - applicationId = result2.applicationId, - webDomain = result2.webDomain, - webScheme = result2.webScheme, - autofillStructure2 = result2, - ), - ) - val intentSender: IntentSender = PendingIntent.getActivity( - this@KeyguardAutofillService, - 1001, - authIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, - ).intentSender + val authIntent = AutofillActivity.getIntent( + context = this@KeyguardAutofillService, + args = AutofillActivity.Args( + applicationId = struct.applicationId, + webDomain = struct.webDomain, + webScheme = struct.webScheme, + autofillStructure2 = struct, + ), + ) + val authIntentSender = PendingIntent.getActivity( + this@KeyguardAutofillService, + 1001, + authIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ).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) { val inlinePresentation = tryCreateAuthenticationInlinePresentation( @@ -460,13 +489,9 @@ class KeyguardAutofillService : AutofillService(), DIAware { inlinePresentation?.let { builder.setInlinePresentation(it) } - Log.e( - "SuggestionsTest", - "adding inline=$inlinePresentation", - ) } - builder.setValue(autofillId, null) - builder.setAuthentication(intentSender) + builder.setValue(item.id, null) + builder.setAuthentication(authIntentSender) addDataset(builder.build()) } } @@ -620,7 +645,7 @@ class KeyguardAutofillService : AutofillService(), DIAware { PendingIntent.getActivity(this, 1010, intent, flags) }, content = { - val title = org.jetbrains.compose.resources.getString(Res.string.autofill_open_keyguard) + val title = getString(Res.string.autofill_open_keyguard) setContentDescription(title) setTitle(title) setStartIcon(createAppIcon()) @@ -630,7 +655,7 @@ class KeyguardAutofillService : AutofillService(), DIAware { @SuppressLint("RestrictedApi") @RequiresApi(Build.VERSION_CODES.R) private suspend fun tryCreateAuthenticationInlinePresentation( - type: FooBar, + type: AuthenticationType, request: FillRequest, index: Int, intent: Intent, @@ -643,8 +668,8 @@ class KeyguardAutofillService : AutofillService(), DIAware { }, content = { val text = when (type) { - FooBar.UNLOCK -> org.jetbrains.compose.resources.getString(Res.string.autofill_unlock_keyguard) - FooBar.SELECT -> org.jetbrains.compose.resources.getString(Res.string.autofill_open_keyguard) + AuthenticationType.UNLOCK -> getString(Res.string.autofill_unlock_keyguard) + AuthenticationType.SELECT -> getString(Res.string.autofill_open_keyguard) } setContentDescription(text) setTitle(text) @@ -689,26 +714,49 @@ class KeyguardAutofillService : AutofillService(), DIAware { } override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { - val autofillStructure = kotlin.runCatching { - val structureLatest = request.fillContexts - .map { it.structure } - .lastOrNull() - // If the structure is missing, then abort auto-filling - // process. - ?: throw IllegalStateException("No structures to fill.") + getSaveStructureIo(request) + .flatMap { autofillStructure -> + if (autofillStructure.items.isEmpty()) { + throw AbortAutofillException("Nothing to autofill.") + } - val respectAutofillOff = prefRespectAutofillOffFlow.toIO().bindBlocking() - autofillStructureParser.parse( - structureLatest, - respectAutofillOff, - ) - }.fold( - onSuccess = ::identity, - onFailure = { - callback.onFailure("Failed to parse structures: ${it.message}") - return - }, - ) + getSaveResponseIo( + request = request, + autofillStructure = autofillStructure, + ) + } + .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 .items .asSequence() @@ -724,11 +772,10 @@ class KeyguardAutofillService : AutofillService(), DIAware { !hintsIncludeUsername || !hintsIncludePassword ) { - callback.onFailure("Can only save login data.") - return + throw AbortAutofillException("Can only save login data.") } - val intent = AutofillSaveActivity.getIntent( + AutofillSaveActivity.getIntent( context = this@KeyguardAutofillService, args = AutofillSaveActivity.Args( applicationId = autofillStructure.applicationId, @@ -737,24 +784,13 @@ class KeyguardAutofillService : AutofillService(), DIAware { 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() { super.onDestroy() job.cancel() } + + private suspend fun getString(res: StringResource) = + org.jetbrains.compose.resources.getString(res) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/AutofillTarget.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/AutofillTarget.kt index a1364e0..fa0e991 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/AutofillTarget.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/AutofillTarget.kt @@ -3,4 +3,5 @@ package com.artemchep.keyguard.common.model data class AutofillTarget( val links: List, val hints: List, + val maxCount: Int = -1, ) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DSecret.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DSecret.kt index 9690d6d..6b94e1c 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DSecret.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DSecret.kt @@ -477,17 +477,28 @@ fun DSecret.contains(hint: AutofillHint) = when (hint) { AutofillHint.POSTAL_ADDRESS_EXTENDED_POSTAL_CODE -> false // not supported AutofillHint.POSTAL_ADDRESS_APT_NUMBER -> identity?.address1 != null AutofillHint.POSTAL_ADDRESS_DEPENDENT_LOCALITY -> false // not supported - AutofillHint.PERSON_NAME -> identity?.firstName != null - AutofillHint.PERSON_NAME_GIVEN -> false // not supported + AutofillHint.PERSON_NAME -> identity?.firstName != null || + identity?.middleName != null || + identity?.lastName != null + + AutofillHint.PERSON_NAME_GIVEN -> identity?.firstName != null AutofillHint.PERSON_NAME_FAMILY -> identity?.lastName != null AutofillHint.PERSON_NAME_MIDDLE -> identity?.middleName != null AutofillHint.PERSON_NAME_MIDDLE_INITIAL -> identity?.middleName != null AutofillHint.PERSON_NAME_PREFIX -> false // not supported AutofillHint.PERSON_NAME_SUFFIX -> false // not supported - AutofillHint.PHONE_NUMBER -> identity?.phone != null - AutofillHint.PHONE_NUMBER_DEVICE -> false // not supported - AutofillHint.PHONE_COUNTRY_CODE -> identity?.phone != null // TODO: Extract country code - AutofillHint.PHONE_NATIONAL -> false // not supported + AutofillHint.PHONE_NUMBER -> identity?.phone != null || + login?.username?.takeIf { REGEX_PHONE_NUMBER.matches(it) } != null + + 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_PASSWORD -> 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_APT_NUMBER -> identity?.address1 AutofillHint.POSTAL_ADDRESS_DEPENDENT_LOCALITY -> null - AutofillHint.PERSON_NAME -> identity?.firstName - AutofillHint.PERSON_NAME_GIVEN -> null + AutofillHint.PERSON_NAME -> listOfNotNull( + 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_MIDDLE -> identity?.middleName AutofillHint.PERSON_NAME_MIDDLE_INITIAL -> identity?.middleName @@ -541,7 +557,9 @@ fun DSecret.get( AutofillHint.PHONE_NUMBER_DEVICE -> null 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_PASSWORD -> null AutofillHint.GENDER -> null @@ -592,9 +610,9 @@ fun DSecret.gett( val variant = variants.removeFirst() val value = get( - hint = variant.hint, - getTotpCode = getTotpCode, - ).attempt().bind().getOrNull() + hint = variant.hint, + getTotpCode = getTotpCode, + ).attempt().bind().getOrNull() if (value.isNullOrEmpty()) { shouldLoop = shouldLoop || variants.isNotEmpty() return@forEach