fix: Magikeyboard Auto search #2233

This commit is contained in:
J-Jamet
2025-10-20 19:41:00 +02:00
parent 24fb3c4c30
commit e5ea1e35aa
14 changed files with 513 additions and 296 deletions

View File

@@ -63,7 +63,6 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecia
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
@@ -486,14 +485,12 @@ class EntryEditActivity : DatabaseLockActivity(),
}
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
// Populate Magikeyboard with entry
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
this,
entry.getEntryInfo(database)
// Build Magikeyboard response with the entry selected
this.buildSpecialModeResponseAndSetResult(
entryInfo = entry.getEntryInfo(database),
extras = buildEntryResult(entry)
)
onValidateSpecialMode()
// Don't keep activity history for entry edition
finishForEntryResult(entry)
}
private fun entryValidatedForAutofill(database: ContextualDatabase, entry: Entry) {

View File

@@ -467,7 +467,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* -------------------------
*/
fun launchForSearchResult(
fun launchForSearch(
context: Context,
searchInfo: SearchInfo
) {
@@ -488,7 +488,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
context: Context,
typeMode: TypeMode,
searchInfo: SearchInfo? = null,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
activityResultLauncher: ActivityResultLauncher<Intent>?,
) {
EntrySelectionHelper.startActivityForSelectionModeResult(
context = context,
@@ -512,10 +512,10 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
) {
EntrySelectionHelper.startActivityForRegistrationModeResult(
context = context,
activityResultLauncher = activityResultLauncher,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
registerInfo = registerInfo,
typeMode = typeMode
typeMode = typeMode,
activityResultLauncher = activityResultLauncher
)
}
}

View File

@@ -915,11 +915,8 @@ class GroupActivity : DatabaseLockActivity(),
private fun entrySelectedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
removeSearch()
// Populate Magikeyboard with entry
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
this,
entry.getEntryInfo(database)
)
// Build response with the entry selected
this.buildSpecialModeResponseAndSetResult(entry.getEntryInfo(database))
onValidateSpecialMode()
}
@@ -1521,7 +1518,7 @@ class GroupActivity : DatabaseLockActivity(),
* Search Launch
* -------------------------
*/
fun launchForSearchResult(
fun launchForSearch(
context: Context,
database: ContextualDatabase,
searchInfo: SearchInfo,
@@ -1536,13 +1533,18 @@ class GroupActivity : DatabaseLockActivity(),
}
}
/*
* -------------------------
* Selection Launch
* -------------------------
*/
fun launchForSelection(
context: Context,
database: ContextualDatabase,
typeMode: TypeMode,
searchInfo: SearchInfo? = null,
autoSearch: Boolean = false,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
activityResultLauncher: ActivityResultLauncher<Intent>?,
) {
if (database.loaded) {
checkTimeAndBuildIntent(context, null) { intent ->
@@ -1610,7 +1612,7 @@ class GroupActivity : DatabaseLockActivity(),
searchAction = { searchInfo ->
// Search action
if (database.loaded) {
launchForSearchResult(activity,
launchForSearch(activity,
database,
searchInfo,
true)
@@ -1632,11 +1634,7 @@ class GroupActivity : DatabaseLockActivity(),
MagikeyboardService.performSelection(
items = items,
actionPopulateKeyboard = { entryInfo ->
// Keyboard populated
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
activity,
entryInfo
)
activity.buildSpecialModeResponseAndSetResult(items)
onValidateSpecialMode()
},
actionEntrySelection = { autoSearch ->
@@ -1645,6 +1643,7 @@ class GroupActivity : DatabaseLockActivity(),
database = database,
typeMode = TypeMode.MAGIKEYBOARD,
searchInfo = searchInfo,
activityResultLauncher = activityResultLauncher,
autoSearch = autoSearch
)
onLaunchActivitySpecialMode()

View File

@@ -809,7 +809,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
hardwareKey: HardwareKey?,
typeMode: TypeMode,
searchInfo: SearchInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
activityResultLauncher: ActivityResultLauncher<Intent>?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSelectionModeResult(
@@ -831,20 +831,20 @@ class MainCredentialActivity : DatabaseModeActivity() {
@Throws(FileNotFoundException::class)
fun launchForRegistration(
activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
typeMode: TypeMode,
registerInfo: RegisterInfo?
registerInfo: RegisterInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
context = activity,
activityResultLauncher = activityResultLauncher,
intent = intent,
typeMode = typeMode,
registerInfo = registerInfo
registerInfo = registerInfo,
activityResultLauncher = activityResultLauncher,
)
}
}

View File

@@ -34,6 +34,7 @@ import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
@@ -43,6 +44,7 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.getParcelableList
import com.kunzisoft.keepass.utils.putEnumExtra
import com.kunzisoft.keepass.utils.putParcelableList
import java.io.IOException
import java.util.UUID
object EntrySelectionHelper {
@@ -233,12 +235,26 @@ object EntrySelectionHelper {
removeExtra(EXTRA_NODE_ID)
}
/**
* Retrieve nodes ids from [intent] and get the corresponding entry info list in [database]
*/
fun Intent.retrieveAndRemoveEntries(database: ContextualDatabase): List<EntryInfo> {
val nodesIds = retrieveNodesIds()
?: throw IOException("NodesIds is null")
removeNodesIds()
return nodesIds.mapNotNull { nodeId ->
database
.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
}
}
/**
* Intent sender uses special retains data in callback
*/
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
return (specialMode == SpecialMode.SELECTION
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
&& (typeMode == TypeMode.MAGIKEYBOARD || typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|| (specialMode == SpecialMode.REGISTRATION
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
}

View File

@@ -45,7 +45,6 @@ import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutof
import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
@@ -87,10 +86,6 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
showBlockRestartMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowReadOnlyMessage -> {
showReadOnlySaveMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
showAutofillSuggestionMessage()
}
@@ -101,8 +96,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
// Retrieve the UI
autofillLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.UIState.Loading -> {}
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
is CredentialLauncherViewModel.CredentialState.Loading -> {}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@AutofillLauncherActivity,
database = uiState.database,
@@ -111,7 +106,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
activityResultLauncher = mAutofillSelectionActivityResultLauncher,
)
}
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@AutofillLauncherActivity,
database = uiState.database,
@@ -120,7 +115,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
activityResultLauncher = mAutofillRegistrationActivityResultLauncher
)
}
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection(
context = this@AutofillLauncherActivity,
searchInfo = uiState.searchInfo,
@@ -128,7 +123,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
activityResultLauncher = mAutofillSelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@AutofillLauncherActivity,
registerInfo = uiState.registerInfo,
@@ -136,14 +131,14 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
)
}
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.UIState.ShowError -> {
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
autofillLauncherViewModel.cancelResult()
}
@@ -174,10 +169,6 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
).show()
}
private fun showReadOnlySaveMessage() {
toastError(RegisterInReadOnlyDatabaseException())
}
companion object {
private val TAG = AutofillLauncherActivity::class.java.name

View File

@@ -22,20 +22,22 @@ package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.net.toUri
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.EntrySelectionViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
/**
* Activity to search or select entry in database,
@@ -43,201 +45,129 @@ import com.kunzisoft.keepass.view.toastError
*/
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
override fun applyCustomStyle(): Boolean {
return false
}
private val entrySelectionViewModel: EntrySelectionViewModel by viewModels()
override fun finishActivityIfReloadRequested(): Boolean {
return false
private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
entrySelectionViewModel.manageSelectionResult(it)
}
override fun applyCustomStyle() = false
override fun finishActivityIfReloadRequested() = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
entrySelectionViewModel.initialize()
lifecycleScope.launch {
// Initialize the parameters
entrySelectionViewModel.uiState.collect { uiState ->
when (uiState) {
is EntrySelectionViewModel.UIState.Loading -> {}
is EntrySelectionViewModel.UIState.PopulateKeyboard -> {
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(
context = this@EntrySelectionLauncherActivity,
entry = uiState.entryInfo,
toast = true
)
}
is EntrySelectionViewModel.UIState.LaunchFileDatabaseSelectForSearch -> {
FileDatabaseSelectActivity.launchForSearch(
context = this@EntrySelectionLauncherActivity,
searchInfo = uiState.searchInfo
)
}
is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> {
GroupActivity.launchForSearch(
context = this@EntrySelectionLauncherActivity,
database = uiState.database,
searchInfo = uiState.searchInfo
)
}
}
}
}
lifecycleScope.launch {
// Retrieve the UI
entrySelectionViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.CredentialState.Loading -> {}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@EntrySelectionLauncherActivity,
database = uiState.database,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mEntrySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@EntrySelectionLauncherActivity,
database = uiState.database,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = null // Null to not get any callback
)
finish()
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection(
context = this@EntrySelectionLauncherActivity,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mEntrySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@EntrySelectionLauncherActivity,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = null // Null to not get any callback
)
finish()
}
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
entrySelectionViewModel.cancelResult()
}
}
}
}
}
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
if (keySelectionBundle != null) {
// To manage package name
var searchInfo = SearchInfo()
keySelectionBundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
searchInfo = mSearchInfo
}
launch(database, searchInfo)
} else {
// To manage share
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain or OTP
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
sharedWebDomain = extra.toUri().host
}
}
launchSelection(database, sharedWebDomain, otpString)
}
Intent.ACTION_VIEW -> {
// Retrieve OTP
intent.dataString?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
}
launchSelection(database, null, otpString)
}
else -> {
if (database != null) {
GroupActivity.launch(this, database)
} else {
FileDatabaseSelectActivity.launch(this)
}
}
}
}
finish()
entrySelectionViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
private fun launchSelection(database: ContextualDatabase?,
sharedWebDomain: String?,
otpString: String?) {
// Build domain search param
val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain
this.otpString = otpString
}
launch(database, searchInfo)
}
private fun launch(database: ContextualDatabase?,
searchInfo: SearchInfo) {
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
// If database is open
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = null,
database = openedDatabase,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else {
toastError(RegisterInReadOnlyDatabaseException())
}
} else if (searchShareForMagikeyboard) {
MagikeyboardService.performSelection(
items,
{ entryInfo ->
// Automatically populate keyboard
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
this,
entryInfo
)
},
{ autoSearch ->
GroupActivity.launchForSelection(
context = this,
database = openedDatabase,
typeMode = TypeMode.MAGIKEYBOARD,
searchInfo = searchInfo,
autoSearch = autoSearch
)
}
)
} else {
GroupActivity.launchForSearchResult(
this,
openedDatabase,
searchInfo,
true
)
}
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = null,
database = openedDatabase,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else {
toastError(RegisterInReadOnlyDatabaseException())
}
} else if (searchShareForMagikeyboard) {
GroupActivity.launchForSelection(
context = this,
database = openedDatabase,
typeMode = TypeMode.MAGIKEYBOARD,
searchInfo = searchInfo,
autoSearch = false
)
} else {
GroupActivity.launchForSearchResult(
this,
openedDatabase,
searchInfo,
false
)
}
},
onDatabaseClosed = {
// If database not open
if (searchInfo.otpString != null) {
FileDatabaseSelectActivity.launchForRegistration(
context = this,
activityResultLauncher = null,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForSelection(
context = this,
typeMode = TypeMode.MAGIKEYBOARD,
searchInfo = searchInfo
)
} else {
FileDatabaseSelectActivity.launchForSearchResult(
this,
searchInfo
)
}
}
)
override fun onDestroy() {
super.onDestroy()
}
companion object {
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
fun launch(context: Context,
searchInfo: SearchInfo? = null) {
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
})
}
// New task needed because don't launch from an Activity context
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
fun launch(
context: Context,
searchInfo: SearchInfo? = null
) {
context.startActivity(Intent(
context,
EntrySelectionLauncherActivity::class.java
).apply {
addSearchInfo(searchInfo)
// New task needed because don't launch from an Activity context
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
})
}
}
}

View File

@@ -76,14 +76,14 @@ class HardwareKeyActivity: DatabaseModeActivity(){
lifecycleScope.launch {
mHardwareKeyLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.UIState.ShowError -> {
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
mHardwareKeyLauncherViewModel.cancelResult()
}

View File

@@ -112,19 +112,19 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
lifecycleScope.launch {
passkeyLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.UIState.Loading -> {}
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
is CredentialLauncherViewModel.CredentialState.Loading -> {}
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.UIState.ShowError -> {
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
passkeyLauncherViewModel.cancelResult()
}
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@PasskeyLauncherActivity,
database = uiState.database,
@@ -133,7 +133,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
activityResultLauncher = mPasskeySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
database = uiState.database,
@@ -142,7 +142,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher
)
}
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection(
context = this@PasskeyLauncherActivity,
typeMode = uiState.typeMode,
@@ -150,7 +150,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
activityResultLauncher = mPasskeySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
typeMode = uiState.typeMode,

View File

@@ -462,9 +462,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
}
fun performSelection(items: List<EntryInfo>,
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
fun performSelection(
items: List<EntryInfo>,
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit
) {
EntrySelectionHelper.performSelection(
items = items,
actionPopulateCredentialProvider = { itemFound ->
@@ -478,15 +480,5 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
actionEntrySelection = actionEntrySelection
)
}
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
entry: EntryInfo,
toast: Boolean = true) {
// Populate Magikeyboard with entry
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode
activity.intent.removeModes()
activity.moveTaskToBack(true)
}
}
}

View File

@@ -9,8 +9,7 @@ import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.annotation.RequiresApi
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeNodesIds
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodesIds
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveAndRemoveEntries
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
@@ -21,7 +20,7 @@ import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.retrieveAutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
@@ -119,7 +118,7 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
CredentialState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.AUTOFILL
@@ -128,7 +127,7 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
onDatabaseClosed = {
// If database not open
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection(
CredentialState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo,
typeMode = TypeMode.AUTOFILL
)
@@ -156,17 +155,10 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
Log.d(TAG, "Autofill selection result")
if (intent == null)
throw IOException("Intent is null")
val nodesIds = intent.retrieveNodesIds()
?: throw IOException("NodesIds is null")
intent.removeNodesIds()
val entries = intent.retrieveAndRemoveEntries(database)
val autofillComponent = mAutofillComponent
if (autofillComponent == null)
throw IOException("Autofill component is null")
val entries = nodesIds.mapNotNull { nodeId ->
database
.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
}
withContext(Dispatchers.Main) {
AutofillHelper.buildResponse(
context = getApplication(),
@@ -211,32 +203,36 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
if (!readOnly) {
// Show the database UI to select the entry
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
mUiState.value = UIState.ShowReadOnlyMessage
mCredentialUiState.value = CredentialState.ShowError(
RegisterInReadOnlyDatabaseException()
)
}
},
onItemNotFound = { openedDatabase ->
if (!readOnly) {
// Show the database UI to select the entry
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
mUiState.value = UIState.ShowReadOnlyMessage
mCredentialUiState.value = CredentialState.ShowError(
RegisterInReadOnlyDatabaseException()
)
}
},
onDatabaseClosed = {
// If database not open
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration(
CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
@@ -274,7 +270,6 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
sealed class UIState {
object Loading: UIState()
object ShowBlockRestartMessage: UIState()
object ShowReadOnlyMessage: UIState()
object ShowAutofillSuggestionMessage: UIState()
}

View File

@@ -25,12 +25,12 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
protected var isResultLauncherRegistered: Boolean = false
private var mSelectionResult: ActivityResult? = null
protected val mCredentialUiState = MutableStateFlow<UIState>(UIState.Loading)
val credentialUiState: StateFlow<UIState> = mCredentialUiState
protected val mCredentialUiState = MutableStateFlow<CredentialState>(CredentialState.Loading)
val credentialUiState: StateFlow<CredentialState> = mCredentialUiState
fun showError(error: Throwable) {
Log.e(TAG, "Error on credential provider launch", error)
mCredentialUiState.value = UIState.ShowError(error)
mCredentialUiState.value = CredentialState.ShowError(error)
}
open fun onResult() {
@@ -41,7 +41,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
fun setResult(intent: Intent, lockDatabase: Boolean = false) {
// Remove the launcher register
onResult()
mCredentialUiState.value = UIState.SetActivityResult(
mCredentialUiState.value = CredentialState.SetActivityResult(
lockDatabase = lockDatabase,
resultCode = RESULT_OK,
data = intent
@@ -50,7 +50,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
fun cancelResult(lockDatabase: Boolean = false) {
onResult()
mCredentialUiState.value = UIState.SetActivityResult(
mCredentialUiState.value = CredentialState.SetActivityResult(
lockDatabase = lockDatabase,
resultCode = RESULT_CANCELED
)
@@ -115,34 +115,34 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
database: ContextualDatabase?
)
sealed class UIState {
object Loading : UIState()
sealed class CredentialState {
object Loading : CredentialState()
data class LaunchGroupActivityForSelection(
val database: ContextualDatabase,
val searchInfo: SearchInfo?,
val typeMode: TypeMode
): UIState()
): CredentialState()
data class LaunchGroupActivityForRegistration(
val database: ContextualDatabase,
val registerInfo: RegisterInfo?,
val typeMode: TypeMode
): UIState()
): CredentialState()
data class LaunchFileDatabaseSelectActivityForSelection(
val searchInfo: SearchInfo?,
val typeMode: TypeMode
): UIState()
): CredentialState()
data class LaunchFileDatabaseSelectActivityForRegistration(
val registerInfo: RegisterInfo?,
val typeMode: TypeMode
): UIState()
): CredentialState()
data class SetActivityResult(
val lockDatabase: Boolean,
val resultCode: Int,
val data: Intent? = null
): UIState()
): CredentialState()
data class ShowError(
val error: Throwable
): UIState()
): CredentialState()
}
companion object {

View File

@@ -0,0 +1,297 @@
package com.kunzisoft.keepass.credentialprovider.viewmodel
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.core.net.toUri
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveAndRemoveEntries
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
class EntrySelectionViewModel(application: Application): CredentialLauncherViewModel(application) {
private var searchShareForMagikeyboard: Boolean = false
private var mLockDatabaseAfterSelection: Boolean = false
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun initialize() {
searchShareForMagikeyboard = getApplication<Application>().isKeyboardActivatedInSettings()
mLockDatabaseAfterSelection = false // TODO Close database after selection
}
override fun launchActionIfNeeded(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
// Launch with database when a nodeId is present
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
super.launchActionIfNeeded(intent, specialMode, database)
}
}
override suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
if (searchInfo != null) {
launch(database, searchInfo)
} else {
// To manage share
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain or OTP
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
sharedWebDomain = extra.toUri().host
}
}
launchSelection(database, sharedWebDomain, otpString)
}
Intent.ACTION_VIEW -> {
// Retrieve OTP
intent.dataString?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
}
launchSelection(database, null, otpString)
}
else -> {
if (database != null && database.loaded) {
mUiState.value = UIState.LaunchGroupActivityForSearch(
database = database,
searchInfo = SearchInfo()
)
} else {
mUiState.value = UIState.LaunchFileDatabaseSelectForSearch(
searchInfo = SearchInfo()
)
}
}
}
}
}
// -------------
// Selection
// -------------
private fun launchSelection(
database: ContextualDatabase?,
sharedWebDomain: String?,
otpString: String?
) {
// Build domain search param
val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain
this.otpString = otpString
}
launch(database, searchInfo)
}
private fun launch(
database: ContextualDatabase?,
searchInfo: SearchInfo
) {
// If database is open
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(
context = getApplication(),
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
mCredentialUiState.value =
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else {
mCredentialUiState.value = CredentialState.ShowError(
RegisterInReadOnlyDatabaseException()
)
}
} else if (searchShareForMagikeyboard) {
MagikeyboardService.performSelection(
items,
{ entryInfo ->
populateKeyboard(entryInfo)
},
{ autoSearch ->
mCredentialUiState.value = CredentialState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.MAGIKEYBOARD
)
}
)
} else {
mUiState.value = UIState.LaunchGroupActivityForSearch(
database = openedDatabase,
searchInfo = searchInfo
)
}
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
mCredentialUiState.value =
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else {
mCredentialUiState.value = CredentialState.ShowError(
RegisterInReadOnlyDatabaseException()
)
}
} else if (searchShareForMagikeyboard) {
mCredentialUiState.value = CredentialState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.MAGIKEYBOARD
)
} else {
mUiState.value = UIState.LaunchGroupActivityForSearch(
database = openedDatabase,
searchInfo = searchInfo
)
}
},
onDatabaseClosed = {
// If database not open
if (searchInfo.otpString != null) {
mCredentialUiState.value = CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else if (searchShareForMagikeyboard) {
mCredentialUiState.value = CredentialState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo,
typeMode = TypeMode.MAGIKEYBOARD
)
} else {
mUiState.value = UIState.LaunchFileDatabaseSelectForSearch(
searchInfo = searchInfo
)
}
}
)
}
private fun populateKeyboard(entryInfo: EntryInfo) {
// Automatically populate keyboard
mUiState.value = UIState.PopulateKeyboard(entryInfo)
setResult(Intent(), lockDatabase = mLockDatabaseAfterSelection)
}
override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult
) {
super.manageSelectionResult(database, activityResult)
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for Magikeyboard", e)
showError(e)
}) {
when (activityResult.resultCode) {
RESULT_OK -> {
withContext(Dispatchers.IO) {
Log.d(TAG, "Magikeyboard selection result")
if (intent == null)
throw IOException("Intent is null")
val entries = intent.retrieveAndRemoveEntries(database)
withContext(Dispatchers.Main) {
// Populate Magikeyboard with entry
entries.firstOrNull()?.let { entryInfo ->
populateKeyboard(entryInfo)
} // TODO Manage multiple entries in Magikeyboard
}
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()
}
}
}
}
}
override fun manageRegistrationResult(activityResult: ActivityResult) {
super.manageRegistrationResult(activityResult)
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for Magikeyboard", e)
showError(e)
}) {
when (activityResult.resultCode) {
RESULT_OK -> {
// Empty data result
// TODO Show Toast indicating value is saved
withContext(Dispatchers.Main) {
setResult(Intent(), lockDatabase = false)
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()
}
}
}
}
}
sealed class UIState {
object Loading: UIState()
data class PopulateKeyboard(
val entryInfo: EntryInfo
): UIState()
data class LaunchFileDatabaseSelectForSearch(
val searchInfo: SearchInfo
): UIState()
data class LaunchGroupActivityForSearch(
val database: ContextualDatabase,
val searchInfo: SearchInfo
): UIState()
}
companion object {
private val TAG = EntrySelectionViewModel::class.java.name
}
}

View File

@@ -238,7 +238,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
"launch manual selection in opened database"
)
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
CredentialState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
@@ -247,7 +247,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database")
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection(
CredentialState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
)
@@ -426,7 +426,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
"but launch manual registration for a new entry"
)
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
@@ -435,7 +435,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database")
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
@@ -444,7 +444,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
onDatabaseClosed = {
Log.d(TAG, "Manual passkey registration in closed database")
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration(
CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)