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?) {
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?) {

View File

@@ -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,14 +104,12 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
sharedWebDomain: String?,
otpString: String?) {
// Build domain search param
getConcreteWebDomain(this, sharedWebDomain) { concreteWebDomain ->
val searchInfo = SearchInfo().apply {
this.webDomain = concreteWebDomain
this.webDomain = sharedWebDomain
this.otpString = otpString
}
launch(database, searchInfo)
}
}
private fun launch(database: ContextualDatabase?,
searchInfo: SearchInfo) {

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.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(

View File

@@ -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<EntryInfo>,
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,13 +523,12 @@ object AutofillHelper {
autofillComponent: AutofillComponent,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>,
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,
@@ -540,11 +537,9 @@ object AutofillHelper {
database = database,
entriesInfo = entriesInfo,
parseResult = result,
concreteWebDomain = concreteWebDomain,
autofillComponent = autofillComponent
)
))
}
} ?: 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.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.AppUtil.getConcreteWebDomain
import org.joda.time.DateTime
@@ -116,10 +115,9 @@ class KeeAutofillService : AutofillService() {
webDomain = parseResult.webDomain,
webDomainBlocklist = webDomainBlocklist)
) {
getConcreteWebDomain(this, parseResult.webDomain) { webDomainWithoutSubDomain ->
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = webDomainWithoutSubDomain
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
@@ -128,30 +126,13 @@ class KeeAutofillService : AutofillService() {
} else {
null
}
launchSelection(mDatabase,
searchInfo,
parseResult,
AutofillComponent(
val autofillComponent = AutofillComponent(
latestStructure,
inlineSuggestionsRequest
),
callback
)
}
}
}
}
private fun launchSelection(
database: ContextualDatabase?,
searchInfo: SearchInfo,
parseResult: StructureParser.Result,
autofillComponent: AutofillComponent,
callback: FillCallback
) {
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
database = mDatabase,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
callback.onSuccess(
@@ -160,7 +141,6 @@ class KeeAutofillService : AutofillService() {
database = openedDatabase,
entriesInfo = items,
parseResult = parseResult,
concreteWebDomain = searchInfo.webDomain,
autofillComponent = autofillComponent
)
)
@@ -177,6 +157,8 @@ class KeeAutofillService : AutofillService() {
}
)
}
}
}
@SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(
@@ -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,10 +385,9 @@ class KeeAutofillService : AutofillService() {
}
// Show UI to save data
getConcreteWebDomain(applicationContext, parseResult.webDomain) { concreteWebDomain ->
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = concreteWebDomain
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
val registerInfo = RegisterInfo(
@@ -423,19 +404,12 @@ class KeeAutofillService : AutofillService() {
}
)
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()
}
callback.onSuccess(intentSender)
}
}
}

View File

@@ -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<CredentialEntry> {
private fun populatePasskeyData(
option: BeginGetPublicKeyCredentialOption,
callback: (List<CredentialEntry>) -> Unit
) {
val passkeyEntries: MutableList<CredentialEntry> = 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<CreateEntry>) -> 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<CreateEntry>) -> 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(

View File

@@ -179,13 +179,13 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
}
withContext(Dispatchers.Main) {
AutofillHelper.buildResponse(
context = getApplication(),
autofillComponent = autofillComponent,
database = database,
entriesInfo = entries
) { intent ->
withContext(Dispatchers.Main) {
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,14 +269,11 @@ 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)
}
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()

View File

@@ -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)
}

View File

@@ -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>(UIState.Loading)
val uiState: StateFlow<UIState> = _uiState
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = 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 ->
override fun onExceptionOccurred(e: Throwable) {
if (e is PrivilegedAllowLists.PrivilegedException) {
showAppPrivilegedDialog(e.temptingApp)
} else {
showError(e)
}
}) {
launchPasskeyAction(intent, specialMode, database)
}
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,7 +219,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "No Passkey found for selection," +
"launch manual selection in opened database"
)
_uiState.value = UIState.LaunchGroupActivityForSelection(
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
@@ -268,8 +228,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
},
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,7 +403,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "Passkey found for registration, " +
"but launch manual registration for a new entry"
)
_uiState.value = UIState.LaunchGroupActivityForRegistration(
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
@@ -451,7 +412,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
},
onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database")
_uiState.value = UIState.LaunchGroupActivityForRegistration(
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
@@ -459,8 +421,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
},
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

View File

@@ -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,18 +88,25 @@ 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()) {
&& !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 = searchInfo.toString()
searchQuery = query
allowEmptyQuery = false
searchInTitles = false
searchInUsernames = false
@@ -88,15 +129,16 @@ object SearchHelper {
max = MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {
searchWithoutUI = true
onItemsFound.invoke(database,
searchGroup.getChildEntriesInfo(database))
onItemsFound.invoke(
database,
searchGroup.getChildEntriesInfo(database)
)
} else
onItemNotFound.invoke(database)
} ?: onItemNotFound.invoke(database)
}
}
}
if (!searchWithoutUI) {
} else
onItemNotFound.invoke(database)
}
}
}
}

View File

@@ -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) {

View File

@@ -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<AndroidPrivilegedApp> {
val packageManager = context.packageManager