fix: Registration and webDomain coroutine

This commit is contained in:
J-Jamet
2025-09-28 23:24:37 +02:00
parent dd0d85e54e
commit abfa7a3f47
12 changed files with 276 additions and 345 deletions

View File

@@ -78,6 +78,25 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Initialize the parameters
autofillLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
AutofillLauncherViewModel.UIState.Loading -> {}
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
showBlockRestartMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowReadOnlyMessage -> {
showReadOnlySaveMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
showAutofillSuggestionMessage()
}
}
}
}
lifecycleScope.launch { lifecycleScope.launch {
// Retrieve the UI // Retrieve the UI
autofillLauncherViewModel.credentialUiState.collect { uiState -> autofillLauncherViewModel.credentialUiState.collect { uiState ->
@@ -132,25 +151,6 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
} }
} }
} }
lifecycleScope.launch {
// Initialize the parameters
autofillLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
AutofillLauncherViewModel.UIState.Loading -> {}
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
showBlockRestartMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowReadOnlyMessage -> {
showReadOnlySaveMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
showAutofillSuggestionMessage()
}
}
}
}
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase?) {

View File

@@ -33,7 +33,6 @@ import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseExcept
import com.kunzisoft.keepass.database.helper.SearchHelper import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.AppUtil.getConcreteWebDomain
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.view.toastError
@@ -105,13 +104,11 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
sharedWebDomain: String?, sharedWebDomain: String?,
otpString: String?) { otpString: String?) {
// Build domain search param // Build domain search param
getConcreteWebDomain(this, sharedWebDomain) { concreteWebDomain -> val searchInfo = SearchInfo().apply {
val searchInfo = SearchInfo().apply { this.webDomain = sharedWebDomain
this.webDomain = concreteWebDomain this.otpString = otpString
this.otpString = otpString
}
launch(database, searchInfo)
} }
launch(database, searchInfo)
} }
private fun launch(database: ContextualDatabase?, private fun launch(database: ContextualDatabase?,

View File

@@ -46,6 +46,7 @@ import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.AppOrigin
@@ -106,7 +107,17 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
nodeId = uiState.nodeId nodeId = uiState.nodeId
) )
} }
is PasskeyLauncherViewModel.UIState.SetActivityResult -> { is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
updateEntry(uiState.oldEntry, uiState.newEntry)
}
}
}
}
lifecycleScope.launch {
passkeyLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.UIState.Loading -> {}
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
setActivityResult( setActivityResult(
typeMode = TypeMode.PASSKEY, typeMode = TypeMode.PASSKEY,
lockDatabase = uiState.lockDatabase, lockDatabase = uiState.lockDatabase,
@@ -114,11 +125,11 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
data = uiState.data data = uiState.data
) )
} }
is PasskeyLauncherViewModel.UIState.ShowError -> { is CredentialLauncherViewModel.UIState.ShowError -> {
toastError(uiState.error) toastError(uiState.error)
passkeyLauncherViewModel.cancelResult() passkeyLauncherViewModel.cancelResult()
} }
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForSelection -> { is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection( GroupActivity.launchForSelection(
context = this@PasskeyLauncherActivity, context = this@PasskeyLauncherActivity,
database = uiState.database, database = uiState.database,
@@ -127,7 +138,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
searchInfo = uiState.searchInfo searchInfo = uiState.searchInfo
) )
} }
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> { is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration( GroupActivity.launchForRegistration(
context = this@PasskeyLauncherActivity, context = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher, activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
@@ -136,7 +147,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
typeMode = uiState.typeMode typeMode = uiState.typeMode
) )
} }
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> { is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection( FileDatabaseSelectActivity.launchForSelection(
context = this@PasskeyLauncherActivity, context = this@PasskeyLauncherActivity,
typeMode = uiState.typeMode, typeMode = uiState.typeMode,
@@ -144,7 +155,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
searchInfo = uiState.searchInfo, searchInfo = uiState.searchInfo,
) )
} }
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> { is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration( FileDatabaseSelectActivity.launchForRegistration(
context = this@PasskeyLauncherActivity, context = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher, activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
@@ -152,9 +163,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
typeMode = uiState.typeMode typeMode = uiState.typeMode
) )
} }
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
updateEntry(uiState.oldEntry, uiState.newEntry)
}
} }
} }
} }
@@ -162,7 +170,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database) passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(

View File

@@ -52,7 +52,6 @@ import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.AppUtil.getConcreteWebDomain
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import java.io.IOException import java.io.IOException
import kotlin.math.min import kotlin.math.min
@@ -380,7 +379,6 @@ object AutofillHelper {
database: ContextualDatabase, database: ContextualDatabase,
entriesInfo: List<EntryInfo>, entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
concreteWebDomain: String?,
autofillComponent: AutofillComponent autofillComponent: AutofillComponent
): FillResponse? { ): FillResponse? {
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
@@ -448,7 +446,7 @@ object AutofillHelper {
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
val searchInfo = SearchInfo().apply { val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId applicationId = parseResult.applicationId
webDomain = concreteWebDomain webDomain = parseResult.webDomain
webScheme = parseResult.webScheme webScheme = parseResult.webScheme
manualSelection = true manualSelection = true
} }
@@ -525,26 +523,23 @@ object AutofillHelper {
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
database: ContextualDatabase, database: ContextualDatabase,
entriesInfo: List<EntryInfo>, entriesInfo: List<EntryInfo>,
onIntentCreated: suspend (Intent) -> Unit onIntentCreated: (Intent) -> Unit
) { ) {
if (entriesInfo.isEmpty()) { if (entriesInfo.isEmpty()) {
throw IOException("No entries found") throw IOException("No entries found")
} else { } else {
StructureParser(autofillComponent.assistStructure).parse()?.let { result -> StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
getConcreteWebDomain(context, result.webDomain) { concreteWebDomain -> // New Response
// New Response onIntentCreated(Intent().putExtra(
onIntentCreated(Intent().putExtra( AutofillManager.EXTRA_AUTHENTICATION_RESULT,
AutofillManager.EXTRA_AUTHENTICATION_RESULT, buildResponse(
buildResponse( context = context,
context = context, database = database,
database = database, entriesInfo = entriesInfo,
entriesInfo = entriesInfo, parseResult = result,
parseResult = result, autofillComponent = autofillComponent
concreteWebDomain = concreteWebDomain, )
autofillComponent = autofillComponent ))
)
))
}
} ?: throw IOException("Unable to parse the structure") } ?: throw IOException("Unable to parse the structure")
} }
} }

View File

@@ -53,7 +53,6 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.AppUtil.getConcreteWebDomain
import org.joda.time.DateTime import org.joda.time.DateTime
@@ -116,68 +115,51 @@ class KeeAutofillService : AutofillService() {
webDomain = parseResult.webDomain, webDomain = parseResult.webDomain,
webDomainBlocklist = webDomainBlocklist) webDomainBlocklist = webDomainBlocklist)
) { ) {
getConcreteWebDomain(this, parseResult.webDomain) { webDomainWithoutSubDomain -> val searchInfo = SearchInfo().apply {
val searchInfo = SearchInfo().apply { applicationId = parseResult.applicationId
applicationId = parseResult.applicationId webDomain = parseResult.webDomain
webDomain = webDomainWithoutSubDomain webScheme = parseResult.webScheme
webScheme = parseResult.webScheme
}
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request)
} else {
null
}
launchSelection(mDatabase,
searchInfo,
parseResult,
AutofillComponent(
latestStructure,
inlineSuggestionsRequest
),
callback
)
} }
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request)
} else {
null
}
val autofillComponent = AutofillComponent(
latestStructure,
inlineSuggestionsRequest
)
SearchHelper.checkAutoSearchInfo(
context = this,
database = mDatabase,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
callback.onSuccess(
AutofillHelper.buildResponse(
context = this,
database = openedDatabase,
entriesInfo = items,
parseResult = parseResult,
autofillComponent = autofillComponent
)
)
},
onItemNotFound = { openedDatabase ->
// Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, autofillComponent, callback)
},
onDatabaseClosed = {
// Show UI if database not open
showUIForEntrySelection(parseResult, null,
searchInfo, autofillComponent, callback)
}
)
} }
} }
} }
private fun launchSelection(
database: ContextualDatabase?,
searchInfo: SearchInfo,
parseResult: StructureParser.Result,
autofillComponent: AutofillComponent,
callback: FillCallback
) {
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
callback.onSuccess(
AutofillHelper.buildResponse(
context = this,
database = openedDatabase,
entriesInfo = items,
parseResult = parseResult,
concreteWebDomain = searchInfo.webDomain,
autofillComponent = autofillComponent
)
)
},
onItemNotFound = { openedDatabase ->
// Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, autofillComponent, callback)
},
onDatabaseClosed = {
// Show UI if database not open
showUIForEntrySelection(parseResult, null,
searchInfo, autofillComponent, callback)
}
)
}
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun showUIForEntrySelection( private fun showUIForEntrySelection(
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
@@ -377,7 +359,7 @@ class KeeAutofillService : AutofillService() {
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
var success = false var success = false
if (askToSaveData) { if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val latestStructure = request.fillContexts.last().structure val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse(true)?.let { parseResult -> StructureParser(latestStructure).parse(true)?.let { parseResult ->
@@ -403,39 +385,31 @@ class KeeAutofillService : AutofillService() {
} }
// Show UI to save data // Show UI to save data
getConcreteWebDomain(applicationContext, parseResult.webDomain) { concreteWebDomain -> val searchInfo = SearchInfo().apply {
val searchInfo = SearchInfo().apply { applicationId = parseResult.applicationId
applicationId = parseResult.applicationId webDomain = parseResult.webDomain
webDomain = concreteWebDomain webScheme = parseResult.webScheme
webScheme = parseResult.webScheme }
val registerInfo = RegisterInfo(
searchInfo = searchInfo,
username = parseResult.usernameValue?.textValue?.toString(),
password = parseResult.passwordValue?.textValue?.toString(),
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
CreditCard(
parseResult.creditCardHolder,
cardNumber,
expiration,
parseResult.cardVerificationValue
)
} }
val registerInfo = RegisterInfo( )
searchInfo = searchInfo,
username = parseResult.usernameValue?.textValue?.toString(),
password = parseResult.passwordValue?.textValue?.toString(),
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
CreditCard(
parseResult.creditCardHolder,
cardNumber,
expiration,
parseResult.cardVerificationValue
)
}
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { AutofillLauncherActivity.getPendingIntentForRegistration(
// TODO Test pending intent this,
AutofillLauncherActivity.getPendingIntentForRegistration( registerInfo
this, )?.intentSender?.let { intentSender ->
registerInfo success = true
)?.intentSender?.let { intentSender -> callback.onSuccess(intentSender)
callback.onSuccess(intentSender)
} ?: callback.onFailure("Unable to launch registration")
} else {
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
success = true
callback.onSuccess()
}
} }
} }
} }

View File

@@ -120,9 +120,9 @@ class PasskeyProviderService : CredentialProviderService() {
for (option in request.beginGetCredentialOptions) { for (option in request.beginGetCredentialOptions) {
when (option) { when (option) {
is BeginGetPublicKeyCredentialOption -> { is BeginGetPublicKeyCredentialOption -> {
credentialEntries.addAll( populatePasskeyData(option) { listCredentials ->
populatePasskeyData(option) credentialEntries.addAll(listCredentials)
) }
return BeginGetCredentialResponse(credentialEntries) return BeginGetCredentialResponse(credentialEntries)
} }
} }
@@ -131,7 +131,10 @@ class PasskeyProviderService : CredentialProviderService() {
return null return null
} }
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> { private fun populatePasskeyData(
option: BeginGetPublicKeyCredentialOption,
callback: (List<CredentialEntry>) -> Unit
) {
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf() val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
@@ -167,6 +170,7 @@ class PasskeyProviderService : CredentialProviderService() {
) )
} }
} }
callback(passkeyEntries)
}, },
onItemNotFound = { _ -> onItemNotFound = { _ ->
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
@@ -189,6 +193,7 @@ class PasskeyProviderService : CredentialProviderService() {
) )
) )
} }
callback(passkeyEntries)
}, },
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Add pending intent for passkey selection in closed database") Log.d(TAG, "Add pending intent for passkey selection in closed database")
@@ -211,9 +216,9 @@ class PasskeyProviderService : CredentialProviderService() {
) )
) )
} }
callback(passkeyEntries)
} }
) )
return passkeyEntries
} }
override fun onBeginCreateCredentialRequest( override fun onBeginCreateCredentialRequest(
@@ -223,7 +228,9 @@ class PasskeyProviderService : CredentialProviderService() {
) { ) {
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called") Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
try { try {
callback.onResult(processCreateCredentialRequest(request)) processCreateCredentialRequest(request) {
callback.onResult(BeginCreateCredentialResponse(it))
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e) Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
toastError(e) toastError(e)
@@ -231,11 +238,14 @@ class PasskeyProviderService : CredentialProviderService() {
} }
} }
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse { private fun processCreateCredentialRequest(
request: BeginCreateCredentialRequest,
callback: (List<CreateEntry>) -> Unit
) {
when (request) { when (request) {
is BeginCreatePublicKeyCredentialRequest -> { is BeginCreatePublicKeyCredentialRequest -> {
// Request is passkey type // Request is passkey type
return handleCreatePasskeyQuery(request) handleCreatePasskeyQuery(request, callback)
} }
} }
// request type not supported // request type not supported
@@ -264,7 +274,10 @@ class PasskeyProviderService : CredentialProviderService() {
} }
} }
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse { private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
callback: (List<CreateEntry>) -> Unit
) {
val databaseName = mDatabase?.name val databaseName = mDatabase?.name
val accountName = val accountName =
if (databaseName?.isBlank() != false) if (databaseName?.isBlank() != false)
@@ -310,6 +323,7 @@ class PasskeyProviderService : CredentialProviderService() {
} }
}*/ }*/
} }
callback(createEntries)
}, },
onItemNotFound = { database -> onItemNotFound = { database ->
// To create a new entry // To create a new entry
@@ -318,6 +332,7 @@ class PasskeyProviderService : CredentialProviderService() {
} else { } else {
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo) createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
} }
callback(createEntries)
}, },
onDatabaseClosed = { onDatabaseClosed = {
// Launch the passkey launcher activity to open the database // Launch the passkey launcher activity to open the database
@@ -335,10 +350,9 @@ class PasskeyProviderService : CredentialProviderService() {
) )
) )
} }
callback(createEntries)
} }
) )
return BeginCreateCredentialResponse(createEntries)
} }
override fun onClearCredentialStateRequest( override fun onClearCredentialStateRequest(

View File

@@ -179,13 +179,13 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
.getEntryById(NodeIdUUID(nodeId)) .getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database) ?.getEntryInfo(database)
} }
AutofillHelper.buildResponse( withContext(Dispatchers.Main) {
context = getApplication(), AutofillHelper.buildResponse(
autofillComponent = autofillComponent, context = getApplication(),
database = database, autofillComponent = autofillComponent,
entriesInfo = entries database = database,
) { intent -> entriesInfo = entries
withContext(Dispatchers.Main) { ) { intent ->
setResult(intent) setResult(intent)
} }
} }
@@ -262,7 +262,6 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
fun manageRegistrationResult( fun manageRegistrationResult(
activityResult: ActivityResult activityResult: ActivityResult
) { ) {
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e -> viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create registration response for autofill", e) Log.e(TAG, "Unable to create registration response for autofill", e)
showError(e) showError(e)
@@ -270,12 +269,9 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
val responseIntent = Intent() val responseIntent = Intent()
when (activityResult.resultCode) { when (activityResult.resultCode) {
RESULT_OK -> { RESULT_OK -> {
withContext(Dispatchers.IO) { Log.d(TAG, "Autofill registration result")
Log.d(TAG, "Autofill registration result") withContext(Dispatchers.Main) {
// TODO Result setResult(responseIntent)
withContext(Dispatchers.Main) {
setResult(responseIntent)
}
} }
} }
RESULT_CANCELED -> { RESULT_CANCELED -> {

View File

@@ -58,6 +58,10 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
mDatabase = database mDatabase = database
} }
open fun onExceptionOccurred(e: Throwable) {
showError(e)
}
fun launchActionIfNeeded( fun launchActionIfNeeded(
intent: Intent, intent: Intent,
specialMode: SpecialMode, specialMode: SpecialMode,
@@ -67,7 +71,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
if (isResultLauncherRegistered.not()) { if (isResultLauncherRegistered.not()) {
isResultLauncherRegistered = true isResultLauncherRegistered = true
viewModelScope.launch(CoroutineExceptionHandler { _, e -> viewModelScope.launch(CoroutineExceptionHandler { _, e ->
showError(e) onExceptionOccurred(e)
}) { }) {
launchAction(intent, specialMode, database) launchAction(intent, specialMode, database)
} }

View File

@@ -11,7 +11,6 @@ import androidx.annotation.RequiresApi
import androidx.credentials.GetCredentialResponse import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialUnknownException import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PendingIntentHandler
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
@@ -56,7 +55,7 @@ import java.io.InvalidObjectException
import java.util.UUID import java.util.UUID
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class PasskeyLauncherViewModel(application: Application): AndroidViewModel(application) { class PasskeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
@@ -64,12 +63,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
private var mBackupEligibility: Boolean = true private var mBackupEligibility: Boolean = true
private var mBackupState: Boolean = false private var mBackupState: Boolean = false
private var mLockDatabase: Boolean = true
private var isResultLauncherRegistered: Boolean = false private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = _uiState
fun initialize() { fun initialize() {
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication()) mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
@@ -79,19 +75,14 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
fun showAppPrivilegedDialog( fun showAppPrivilegedDialog(
temptingApp: AndroidPrivilegedApp temptingApp: AndroidPrivilegedApp
) { ) {
_uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp) mUiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
} }
fun showAppSignatureDialog( fun showAppSignatureDialog(
temptingApp: AppOrigin, temptingApp: AppOrigin,
nodeId: UUID nodeId: UUID
) { ) {
_uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId) mUiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
}
fun showError(error: Throwable) {
Log.e(TAG, "Error on passkey launch", error)
_uiState.value = UIState.ShowError(error)
} }
fun saveCustomPrivilegedApp( fun saveCustomPrivilegedApp(
@@ -107,7 +98,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
context = getApplication(), context = getApplication(),
privilegedApps = listOf(temptingApp) privilegedApps = listOf(temptingApp)
) )
launchPasskeyAction( launchAction(
intent = intent, intent = intent,
specialMode = specialMode, specialMode = specialMode,
database = database database = database
@@ -139,54 +130,22 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
) )
entryInfo.saveAppOrigin(database, temptingApp) entryInfo.saveAppOrigin(database, temptingApp)
newEntry.setEntryInfo(database, entryInfo) newEntry.setEntryInfo(database, entryInfo)
_uiState.value = UIState.UpdateEntry( mUiState.value = UIState.UpdateEntry(
oldEntry = entry, oldEntry = entry,
newEntry = newEntry newEntry = newEntry
) )
} }
} }
fun setResult(intent: Intent) { override fun onExceptionOccurred(e: Throwable) {
// Remove the launcher register if (e is PrivilegedAllowLists.PrivilegedException) {
isResultLauncherRegistered = false showAppPrivilegedDialog(e.temptingApp)
_uiState.value = UIState.SetActivityResult( } else {
lockDatabase = mLockDatabase, super.onExceptionOccurred(e)
resultCode = RESULT_OK,
data = intent
)
}
fun cancelResult() {
isResultLauncherRegistered = false
_uiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_CANCELED
)
}
fun launchPasskeyActionIfNeeded(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
if (isResultLauncherRegistered.not()) {
isResultLauncherRegistered = true
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
if (e is PrivilegedAllowLists.PrivilegedException) {
showAppPrivilegedDialog(e.temptingApp)
} else {
showError(e)
}
}) {
launchPasskeyAction(intent, specialMode, database)
}
} }
} }
/** override suspend fun launchAction(
* Launch the main action to manage Passkey
*/
private suspend fun launchPasskeyAction(
intent: Intent, intent: Intent,
specialMode: SpecialMode, specialMode: SpecialMode,
database: ContextualDatabase? database: ContextualDatabase?
@@ -260,16 +219,17 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "No Passkey found for selection," + TAG, "No Passkey found for selection," +
"launch manual selection in opened database" "launch manual selection in opened database"
) )
_uiState.value = UIState.LaunchGroupActivityForSelection( mCredentialUiState.value =
database = openedDatabase, CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
searchInfo = searchInfo, database = openedDatabase,
typeMode = TypeMode.PASSKEY searchInfo = searchInfo,
) typeMode = TypeMode.PASSKEY
)
}, },
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database") Log.d(TAG, "Manual passkey selection in closed database")
_uiState.value = mCredentialUiState.value =
UIState.LaunchFileDatabaseSelectActivityForSelection( CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo, searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY typeMode = TypeMode.PASSKEY
) )
@@ -443,24 +403,26 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "Passkey found for registration, " + TAG, "Passkey found for registration, " +
"but launch manual registration for a new entry" "but launch manual registration for a new entry"
) )
_uiState.value = UIState.LaunchGroupActivityForRegistration( mCredentialUiState.value =
database = openedDatabase, CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
registerInfo = registerInfo, database = openedDatabase,
typeMode = TypeMode.PASSKEY registerInfo = registerInfo,
) typeMode = TypeMode.PASSKEY
)
}, },
onItemNotFound = { openedDatabase -> onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database") Log.d(TAG, "Launch new manual registration in opened database")
_uiState.value = UIState.LaunchGroupActivityForRegistration( mCredentialUiState.value =
database = openedDatabase, CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
registerInfo = registerInfo, database = openedDatabase,
typeMode = TypeMode.PASSKEY registerInfo = registerInfo,
) typeMode = TypeMode.PASSKEY
)
}, },
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Manual passkey registration in closed database") Log.d(TAG, "Manual passkey registration in closed database")
_uiState.value = mCredentialUiState.value =
UIState.LaunchFileDatabaseSelectActivityForRegistration( CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo, registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY typeMode = TypeMode.PASSKEY
) )
@@ -552,32 +514,6 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val temptingApp: AppOrigin, val temptingApp: AppOrigin,
val nodeId: UUID val nodeId: UUID
): UIState() ): UIState()
data class LaunchGroupActivityForSelection(
val database: ContextualDatabase,
val searchInfo: SearchInfo?,
val typeMode: TypeMode
): UIState()
data class LaunchGroupActivityForRegistration(
val database: ContextualDatabase,
val registerInfo: RegisterInfo,
val typeMode: TypeMode
): UIState()
data class LaunchFileDatabaseSelectActivityForSelection(
val searchInfo: SearchInfo,
val typeMode: TypeMode
): UIState()
data class LaunchFileDatabaseSelectActivityForRegistration(
val registerInfo: RegisterInfo,
val typeMode: TypeMode
): UIState()
data class SetActivityResult(
val lockDatabase: Boolean,
val resultCode: Int,
val data: Intent? = null
): UIState()
data class ShowError(
val error: Throwable
): UIState()
data class UpdateEntry( data class UpdateEntry(
val oldEntry: Entry, val oldEntry: Entry,
val newEntry: Entry val newEntry: Entry

View File

@@ -26,6 +26,11 @@ import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
object SearchHelper { object SearchHelper {
@@ -42,6 +47,35 @@ object SearchHelper {
} }
} }
/**
* Get the concrete web domain AKA without sub domain if needed
*/
private fun getConcreteWebDomain(
context: Context,
webDomain: String?,
concreteWebDomain: (String?) -> Unit
) {
val domain = webDomain
if (domain != null) {
// Warning, web domain can contains IP, don't crop in this case
if (searchSubDomains(context)
|| Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
concreteWebDomain.invoke(webDomain)
} else {
CoroutineScope(Dispatchers.IO).launch {
val publicSuffixList = PublicSuffixList(context)
val publicSuffix = publicSuffixList
.getPublicSuffixPlusOne(domain).await()
withContext(Dispatchers.Main) {
concreteWebDomain.invoke(publicSuffix)
}
}
}
} else {
concreteWebDomain.invoke(null)
}
}
/** /**
* Utility method to perform actions if item is found or not after an auto search in [database] * Utility method to perform actions if item is found or not after an auto search in [database]
*/ */
@@ -54,49 +88,57 @@ object SearchHelper {
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit, onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
onDatabaseClosed: () -> Unit onDatabaseClosed: () -> Unit
) { ) {
// TODO suspend // Do not place coroutine at start, bug in Passkey implementation
if (database == null || !database.loaded) { if (database == null || !database.loaded) {
onDatabaseClosed.invoke() onDatabaseClosed.invoke()
} else if (TimeoutHelper.checkTime(context)) { } else if (TimeoutHelper.checkTime(context)) {
var searchWithoutUI = false
if (searchInfo != null if (searchInfo != null
&& !searchInfo.manualSelection && !searchInfo.manualSelection
&& !searchInfo.containsOnlyNullValues()) { && !searchInfo.containsOnlyNullValues()
// If search provide results ) {
database.createVirtualGroupFromSearchInfo( getConcreteWebDomain(
searchParameters = SearchParameters().apply { context,
searchQuery = searchInfo.toString() searchInfo.webDomain
allowEmptyQuery = false ) { concreteDomain ->
searchInTitles = false var query = searchInfo.toString()
searchInUsernames = false if (searchInfo.isDomainSearch && concreteDomain != null)
searchInPasswords = false query = concreteDomain
searchInAppIds = searchInfo.isAppIdSearch // If search provide results
searchInUrls = searchInfo.isDomainSearch database.createVirtualGroupFromSearchInfo(
searchByDomain = true searchParameters = SearchParameters().apply {
searchBySubDomain = searchSubDomains(context) searchQuery = query
searchInRelyingParty = searchInfo.isPasskeySearch allowEmptyQuery = false
searchInNotes = false searchInTitles = false
searchInOTP = searchInfo.isOTPSearch searchInUsernames = false
searchInOther = false searchInPasswords = false
searchInUUIDs = false searchInAppIds = searchInfo.isAppIdSearch
searchInTags = searchInfo.isTagSearch searchInUrls = searchInfo.isDomainSearch
searchInCurrentGroup = false searchByDomain = true
searchInSearchableGroup = true searchBySubDomain = searchSubDomains(context)
searchInRecycleBin = false searchInRelyingParty = searchInfo.isPasskeySearch
searchInTemplates = false searchInNotes = false
}, searchInOTP = searchInfo.isOTPSearch
max = MAX_SEARCH_ENTRY searchInOther = false
)?.let { searchGroup -> searchInUUIDs = false
if (searchGroup.numberOfChildEntries > 0) { searchInTags = searchInfo.isTagSearch
searchWithoutUI = true searchInCurrentGroup = false
onItemsFound.invoke(database, searchInSearchableGroup = true
searchGroup.getChildEntriesInfo(database)) searchInRecycleBin = false
} searchInTemplates = false
},
max = MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {
onItemsFound.invoke(
database,
searchGroup.getChildEntriesInfo(database)
)
} else
onItemNotFound.invoke(database)
} ?: onItemNotFound.invoke(database)
} }
} } else
if (!searchWithoutUI) {
onItemNotFound.invoke(database) onItemNotFound.invoke(database)
}
} }
} }
} }

View File

@@ -41,6 +41,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
autofillInlineSuggestionsPreference?.isVisible = false autofillInlineSuggestionsPreference?.isVisible = false
} }
val autofillAskSaveDataPreference: TwoStatePreference? = findPreference(getString(R.string.autofill_ask_to_save_data_key))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
autofillAskSaveDataPreference?.isVisible = false
}
} }
override fun onDisplayPreferenceDialog(preference: Preference) { override fun onDisplayPreferenceDialog(preference: Preference) {

View File

@@ -13,13 +13,6 @@ import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
object AppUtil { object AppUtil {
@@ -84,39 +77,6 @@ object AppUtil {
) )
} }
/**
* Get the concrete web domain AKA without sub domain if needed
*/
fun getConcreteWebDomain(
context: Context,
webDomain: String?,
concreteWebDomain: suspend (String?) -> Unit
) {
CoroutineScope(Dispatchers.IO).launch {
val domain = webDomain
if (domain != null) {
// Warning, web domain can contains IP, don't crop in this case
if (PreferencesUtil.searchSubDomains(context)
|| Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
withContext(Dispatchers.Main) {
concreteWebDomain.invoke(webDomain)
}
} else {
val publicSuffixList = PublicSuffixList(context)
val publicSuffix = publicSuffixList
.getPublicSuffixPlusOne(domain).await()
withContext(Dispatchers.Main) {
concreteWebDomain.invoke(publicSuffix)
}
}
} else {
withContext(Dispatchers.Main) {
concreteWebDomain.invoke(null)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.P) @RequiresApi(Build.VERSION_CODES.P)
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> { fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
val packageManager = context.packageManager val packageManager = context.packageManager