diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt index a4859f526..9c86b1bac 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt @@ -78,6 +78,25 @@ class AutofillLauncherActivity : DatabaseModeActivity() { override fun onCreate(savedInstanceState: Bundle?) { 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 { // Retrieve the UI 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?) { diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt index 608a1fcbb..9a5982321 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt @@ -33,7 +33,6 @@ import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseExcept import com.kunzisoft.keepass.database.helper.SearchHelper import com.kunzisoft.keepass.model.SearchInfo 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.getParcelableCompat import com.kunzisoft.keepass.view.toastError @@ -105,13 +104,11 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() { sharedWebDomain: String?, otpString: String?) { // Build domain search param - getConcreteWebDomain(this, sharedWebDomain) { concreteWebDomain -> - val searchInfo = SearchInfo().apply { - this.webDomain = concreteWebDomain - this.otpString = otpString - } - launch(database, searchInfo) + val searchInfo = SearchInfo().apply { + this.webDomain = sharedWebDomain + this.otpString = otpString } + launch(database, searchInfo) } private fun launch(database: ContextualDatabase?, diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt index e93a0f66d..47eda6671 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt @@ -46,6 +46,7 @@ import com.kunzisoft.keepass.credentialprovider.TypeMode 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.addAuthCode +import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.model.AppOrigin @@ -106,7 +107,17 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { 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( typeMode = TypeMode.PASSKEY, lockDatabase = uiState.lockDatabase, @@ -114,11 +125,11 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { data = uiState.data ) } - is PasskeyLauncherViewModel.UIState.ShowError -> { + is CredentialLauncherViewModel.UIState.ShowError -> { toastError(uiState.error) passkeyLauncherViewModel.cancelResult() } - is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForSelection -> { + is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> { GroupActivity.launchForSelection( context = this@PasskeyLauncherActivity, database = uiState.database, @@ -127,7 +138,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { searchInfo = uiState.searchInfo ) } - is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> { + is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> { GroupActivity.launchForRegistration( context = this@PasskeyLauncherActivity, activityResultLauncher = mPasskeyRegistrationActivityResultLauncher, @@ -136,7 +147,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { typeMode = uiState.typeMode ) } - is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> { + is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> { FileDatabaseSelectActivity.launchForSelection( context = this@PasskeyLauncherActivity, typeMode = uiState.typeMode, @@ -144,7 +155,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { searchInfo = uiState.searchInfo, ) } - is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> { + is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> { FileDatabaseSelectActivity.launchForRegistration( context = this@PasskeyLauncherActivity, activityResultLauncher = mPasskeyRegistrationActivityResultLauncher, @@ -152,9 +163,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { typeMode = uiState.typeMode ) } - is PasskeyLauncherViewModel.UIState.UpdateEntry -> { - updateEntry(uiState.oldEntry, uiState.newEntry) - } } } } @@ -162,7 +170,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { override fun onDatabaseRetrieved(database: ContextualDatabase?) { super.onDatabaseRetrieved(database) - passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database) + passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database) } override fun onDatabaseActionFinished( diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt index c7b8e9b9e..ba4125ee3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt @@ -52,7 +52,6 @@ import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.utils.AppUtil.getConcreteWebDomain import com.kunzisoft.keepass.utils.getParcelableExtraCompat import java.io.IOException import kotlin.math.min @@ -380,7 +379,6 @@ object AutofillHelper { database: ContextualDatabase, entriesInfo: List, parseResult: StructureParser.Result, - concreteWebDomain: String?, autofillComponent: AutofillComponent ): FillResponse? { val responseBuilder = FillResponse.Builder() @@ -448,7 +446,7 @@ object AutofillHelper { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) { val searchInfo = SearchInfo().apply { applicationId = parseResult.applicationId - webDomain = concreteWebDomain + webDomain = parseResult.webDomain webScheme = parseResult.webScheme manualSelection = true } @@ -525,26 +523,23 @@ object AutofillHelper { autofillComponent: AutofillComponent, database: ContextualDatabase, entriesInfo: List, - onIntentCreated: suspend (Intent) -> Unit + onIntentCreated: (Intent) -> Unit ) { if (entriesInfo.isEmpty()) { throw IOException("No entries found") } else { StructureParser(autofillComponent.assistStructure).parse()?.let { result -> - getConcreteWebDomain(context, result.webDomain) { concreteWebDomain -> - // New Response - onIntentCreated(Intent().putExtra( - AutofillManager.EXTRA_AUTHENTICATION_RESULT, - buildResponse( - context = context, - database = database, - entriesInfo = entriesInfo, - parseResult = result, - concreteWebDomain = concreteWebDomain, - autofillComponent = autofillComponent - ) - )) - } + // New Response + onIntentCreated(Intent().putExtra( + AutofillManager.EXTRA_AUTHENTICATION_RESULT, + buildResponse( + context = context, + database = database, + entriesInfo = entriesInfo, + parseResult = result, + autofillComponent = autofillComponent + ) + )) } ?: throw IOException("Unable to parse the structure") } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt index 1816e989a..48e462230 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt @@ -53,7 +53,6 @@ import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.utils.AppUtil.getConcreteWebDomain import org.joda.time.DateTime @@ -116,68 +115,51 @@ class KeeAutofillService : AutofillService() { webDomain = parseResult.webDomain, webDomainBlocklist = webDomainBlocklist) ) { - getConcreteWebDomain(this, parseResult.webDomain) { webDomainWithoutSubDomain -> - val searchInfo = SearchInfo().apply { - applicationId = parseResult.applicationId - webDomain = webDomainWithoutSubDomain - 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 searchInfo = SearchInfo().apply { + applicationId = parseResult.applicationId + webDomain = parseResult.webDomain + webScheme = parseResult.webScheme } + 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") private fun showUIForEntrySelection( parseResult: StructureParser.Result, @@ -377,7 +359,7 @@ class KeeAutofillService : AutofillService() { override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { var success = false - if (askToSaveData) { + if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val latestStructure = request.fillContexts.last().structure StructureParser(latestStructure).parse(true)?.let { parseResult -> @@ -403,39 +385,31 @@ class KeeAutofillService : AutofillService() { } // Show UI to save data - getConcreteWebDomain(applicationContext, parseResult.webDomain) { concreteWebDomain -> - val searchInfo = SearchInfo().apply { - applicationId = parseResult.applicationId - webDomain = concreteWebDomain - webScheme = parseResult.webScheme + val searchInfo = SearchInfo().apply { + applicationId = parseResult.applicationId + webDomain = parseResult.webDomain + 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) { - // TODO Test pending intent - AutofillLauncherActivity.getPendingIntentForRegistration( - this, - registerInfo - )?.intentSender?.let { intentSender -> - callback.onSuccess(intentSender) - } ?: callback.onFailure("Unable to launch registration") - } else { - AutofillLauncherActivity.launchForRegistration(this, registerInfo) - success = true - callback.onSuccess() - } + AutofillLauncherActivity.getPendingIntentForRegistration( + this, + registerInfo + )?.intentSender?.let { intentSender -> + success = true + callback.onSuccess(intentSender) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt index af0d1e312..57d158d85 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt @@ -120,9 +120,9 @@ class PasskeyProviderService : CredentialProviderService() { for (option in request.beginGetCredentialOptions) { when (option) { is BeginGetPublicKeyCredentialOption -> { - credentialEntries.addAll( - populatePasskeyData(option) - ) + populatePasskeyData(option) { listCredentials -> + credentialEntries.addAll(listCredentials) + } return BeginGetCredentialResponse(credentialEntries) } } @@ -131,7 +131,10 @@ class PasskeyProviderService : CredentialProviderService() { return null } - private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List { + private fun populatePasskeyData( + option: BeginGetPublicKeyCredentialOption, + callback: (List) -> Unit + ) { val passkeyEntries: MutableList = mutableListOf() @@ -167,6 +170,7 @@ class PasskeyProviderService : CredentialProviderService() { ) } } + callback(passkeyEntries) }, onItemNotFound = { _ -> Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") @@ -189,6 +193,7 @@ class PasskeyProviderService : CredentialProviderService() { ) ) } + callback(passkeyEntries) }, onDatabaseClosed = { 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( @@ -223,7 +228,9 @@ class PasskeyProviderService : CredentialProviderService() { ) { Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called") try { - callback.onResult(processCreateCredentialRequest(request)) + processCreateCredentialRequest(request) { + callback.onResult(BeginCreateCredentialResponse(it)) + } } catch (e: Exception) { Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e) toastError(e) @@ -231,11 +238,14 @@ class PasskeyProviderService : CredentialProviderService() { } } - private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse { + private fun processCreateCredentialRequest( + request: BeginCreateCredentialRequest, + callback: (List) -> Unit + ) { when (request) { is BeginCreatePublicKeyCredentialRequest -> { // Request is passkey type - return handleCreatePasskeyQuery(request) + handleCreatePasskeyQuery(request, callback) } } // request type not supported @@ -264,7 +274,10 @@ class PasskeyProviderService : CredentialProviderService() { } } - private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse { + private fun handleCreatePasskeyQuery( + request: BeginCreatePublicKeyCredentialRequest, + callback: (List) -> Unit + ) { val databaseName = mDatabase?.name val accountName = if (databaseName?.isBlank() != false) @@ -310,6 +323,7 @@ class PasskeyProviderService : CredentialProviderService() { } }*/ } + callback(createEntries) }, onItemNotFound = { database -> // To create a new entry @@ -318,6 +332,7 @@ class PasskeyProviderService : CredentialProviderService() { } else { createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo) } + callback(createEntries) }, onDatabaseClosed = { // Launch the passkey launcher activity to open the database @@ -335,10 +350,9 @@ class PasskeyProviderService : CredentialProviderService() { ) ) } + callback(createEntries) } ) - - return BeginCreateCredentialResponse(createEntries) } override fun onClearCredentialStateRequest( diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/AutofillLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/AutofillLauncherViewModel.kt index b9c156d34..6771b1d46 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/AutofillLauncherViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/AutofillLauncherViewModel.kt @@ -179,13 +179,13 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie .getEntryById(NodeIdUUID(nodeId)) ?.getEntryInfo(database) } - AutofillHelper.buildResponse( - context = getApplication(), - autofillComponent = autofillComponent, - database = database, - entriesInfo = entries - ) { intent -> - withContext(Dispatchers.Main) { + withContext(Dispatchers.Main) { + AutofillHelper.buildResponse( + context = getApplication(), + autofillComponent = autofillComponent, + database = database, + entriesInfo = entries + ) { intent -> setResult(intent) } } @@ -262,7 +262,6 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie fun manageRegistrationResult( activityResult: ActivityResult ) { - val intent = activityResult.data viewModelScope.launch(CoroutineExceptionHandler { _, e -> Log.e(TAG, "Unable to create registration response for autofill", e) showError(e) @@ -270,12 +269,9 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie val responseIntent = Intent() when (activityResult.resultCode) { RESULT_OK -> { - withContext(Dispatchers.IO) { - Log.d(TAG, "Autofill registration result") - // TODO Result - withContext(Dispatchers.Main) { - setResult(responseIntent) - } + Log.d(TAG, "Autofill registration result") + withContext(Dispatchers.Main) { + setResult(responseIntent) } } RESULT_CANCELED -> { diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt index a48d4a4c3..678eb1253 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt @@ -58,6 +58,10 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie mDatabase = database } + open fun onExceptionOccurred(e: Throwable) { + showError(e) + } + fun launchActionIfNeeded( intent: Intent, specialMode: SpecialMode, @@ -67,7 +71,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie if (isResultLauncherRegistered.not()) { isResultLauncherRegistered = true viewModelScope.launch(CoroutineExceptionHandler { _, e -> - showError(e) + onExceptionOccurred(e) }) { launchAction(intent, specialMode, database) } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt index a0fd93829..67725547e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt @@ -11,7 +11,6 @@ import androidx.annotation.RequiresApi import androidx.credentials.GetCredentialResponse import androidx.credentials.exceptions.GetCredentialUnknownException import androidx.credentials.provider.PendingIntentHandler -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo @@ -56,7 +55,7 @@ import java.io.InvalidObjectException import java.util.UUID @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 mCreationParameters: PublicKeyCredentialCreationParameters? = null @@ -64,12 +63,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli private var mBackupEligibility: Boolean = true private var mBackupState: Boolean = false - private var mLockDatabase: Boolean = true - private var isResultLauncherRegistered: Boolean = false - - private val _uiState = MutableStateFlow(UIState.Loading) - val uiState: StateFlow = _uiState + private val mUiState = MutableStateFlow(UIState.Loading) + val uiState: StateFlow = mUiState fun initialize() { mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication()) @@ -79,19 +75,14 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli fun showAppPrivilegedDialog( temptingApp: AndroidPrivilegedApp ) { - _uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp) + mUiState.value = UIState.ShowAppPrivilegedDialog(temptingApp) } fun showAppSignatureDialog( temptingApp: AppOrigin, nodeId: UUID ) { - _uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId) - } - - fun showError(error: Throwable) { - Log.e(TAG, "Error on passkey launch", error) - _uiState.value = UIState.ShowError(error) + mUiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId) } fun saveCustomPrivilegedApp( @@ -107,7 +98,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli context = getApplication(), privilegedApps = listOf(temptingApp) ) - launchPasskeyAction( + launchAction( intent = intent, specialMode = specialMode, database = database @@ -139,54 +130,22 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli ) entryInfo.saveAppOrigin(database, temptingApp) newEntry.setEntryInfo(database, entryInfo) - _uiState.value = UIState.UpdateEntry( + mUiState.value = UIState.UpdateEntry( oldEntry = entry, newEntry = newEntry ) } } - fun setResult(intent: Intent) { - // Remove the launcher register - isResultLauncherRegistered = false - _uiState.value = UIState.SetActivityResult( - lockDatabase = mLockDatabase, - 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 fun onExceptionOccurred(e: Throwable) { + if (e is PrivilegedAllowLists.PrivilegedException) { + showAppPrivilegedDialog(e.temptingApp) + } else { + super.onExceptionOccurred(e) } } - /** - * Launch the main action to manage Passkey - */ - private suspend fun launchPasskeyAction( + override suspend fun launchAction( intent: Intent, specialMode: SpecialMode, database: ContextualDatabase? @@ -260,16 +219,17 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli TAG, "No Passkey found for selection," + "launch manual selection in opened database" ) - _uiState.value = UIState.LaunchGroupActivityForSelection( - database = openedDatabase, - searchInfo = searchInfo, - typeMode = TypeMode.PASSKEY - ) + mCredentialUiState.value = + CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection( + database = openedDatabase, + searchInfo = searchInfo, + typeMode = TypeMode.PASSKEY + ) }, onDatabaseClosed = { Log.d(TAG, "Manual passkey selection in closed database") - _uiState.value = - UIState.LaunchFileDatabaseSelectActivityForSelection( + mCredentialUiState.value = + CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection( searchInfo = searchInfo, typeMode = TypeMode.PASSKEY ) @@ -443,24 +403,26 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli TAG, "Passkey found for registration, " + "but launch manual registration for a new entry" ) - _uiState.value = UIState.LaunchGroupActivityForRegistration( - database = openedDatabase, - registerInfo = registerInfo, - typeMode = TypeMode.PASSKEY - ) + mCredentialUiState.value = + CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration( + database = openedDatabase, + registerInfo = registerInfo, + typeMode = TypeMode.PASSKEY + ) }, onItemNotFound = { openedDatabase -> Log.d(TAG, "Launch new manual registration in opened database") - _uiState.value = UIState.LaunchGroupActivityForRegistration( - database = openedDatabase, - registerInfo = registerInfo, - typeMode = TypeMode.PASSKEY - ) + mCredentialUiState.value = + CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration( + database = openedDatabase, + registerInfo = registerInfo, + typeMode = TypeMode.PASSKEY + ) }, onDatabaseClosed = { Log.d(TAG, "Manual passkey registration in closed database") - _uiState.value = - UIState.LaunchFileDatabaseSelectActivityForRegistration( + mCredentialUiState.value = + CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration( registerInfo = registerInfo, typeMode = TypeMode.PASSKEY ) @@ -552,32 +514,6 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli val temptingApp: AppOrigin, val nodeId: UUID ): 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( val oldEntry: Entry, val newEntry: Entry diff --git a/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt index 8a95d3949..3fa040b50 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt @@ -26,6 +26,11 @@ import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains 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 { @@ -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] */ @@ -54,49 +88,57 @@ object SearchHelper { onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit, onDatabaseClosed: () -> Unit ) { - // TODO suspend + // Do not place coroutine at start, bug in Passkey implementation if (database == null || !database.loaded) { onDatabaseClosed.invoke() } else if (TimeoutHelper.checkTime(context)) { - var searchWithoutUI = false if (searchInfo != null && !searchInfo.manualSelection - && !searchInfo.containsOnlyNullValues()) { - // If search provide results - database.createVirtualGroupFromSearchInfo( - searchParameters = SearchParameters().apply { - searchQuery = searchInfo.toString() - allowEmptyQuery = false - searchInTitles = false - searchInUsernames = false - searchInPasswords = false - searchInAppIds = searchInfo.isAppIdSearch - searchInUrls = searchInfo.isDomainSearch - searchByDomain = true - searchBySubDomain = searchSubDomains(context) - searchInRelyingParty = searchInfo.isPasskeySearch - searchInNotes = false - searchInOTP = searchInfo.isOTPSearch - searchInOther = false - searchInUUIDs = false - searchInTags = searchInfo.isTagSearch - searchInCurrentGroup = false - searchInSearchableGroup = true - searchInRecycleBin = false - searchInTemplates = false - }, - max = MAX_SEARCH_ENTRY - )?.let { searchGroup -> - if (searchGroup.numberOfChildEntries > 0) { - searchWithoutUI = true - onItemsFound.invoke(database, - searchGroup.getChildEntriesInfo(database)) - } + && !searchInfo.containsOnlyNullValues() + ) { + getConcreteWebDomain( + context, + searchInfo.webDomain + ) { concreteDomain -> + var query = searchInfo.toString() + if (searchInfo.isDomainSearch && concreteDomain != null) + query = concreteDomain + // If search provide results + database.createVirtualGroupFromSearchInfo( + searchParameters = SearchParameters().apply { + searchQuery = query + allowEmptyQuery = false + searchInTitles = false + searchInUsernames = false + searchInPasswords = false + searchInAppIds = searchInfo.isAppIdSearch + searchInUrls = searchInfo.isDomainSearch + searchByDomain = true + searchBySubDomain = searchSubDomains(context) + searchInRelyingParty = searchInfo.isPasskeySearch + searchInNotes = false + searchInOTP = searchInfo.isOTPSearch + searchInOther = false + searchInUUIDs = false + searchInTags = searchInfo.isTagSearch + searchInCurrentGroup = false + searchInSearchableGroup = true + 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) } - } - if (!searchWithoutUI) { + } else onItemNotFound.invoke(database) - } } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt index be5844324..6e0beb5d8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt @@ -41,6 +41,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { 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) { diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt index 855174944..88e3741ef 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt @@ -13,13 +13,6 @@ import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.R import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp 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 { @@ -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) fun getInstalledBrowsersWithSignatures(context: Context): List { val packageManager = context.packageManager