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

View File

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

View File

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

View File

@@ -809,7 +809,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
hardwareKey: HardwareKey?, hardwareKey: HardwareKey?,
typeMode: TypeMode, typeMode: TypeMode,
searchInfo: SearchInfo?, searchInfo: SearchInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>? = null, activityResultLauncher: ActivityResultLauncher<Intent>?
) { ) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSelectionModeResult( EntrySelectionHelper.startActivityForSelectionModeResult(
@@ -831,20 +831,20 @@ class MainCredentialActivity : DatabaseModeActivity() {
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launchForRegistration( fun launchForRegistration(
activity: Activity, activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?, hardwareKey: HardwareKey?,
typeMode: TypeMode, typeMode: TypeMode,
registerInfo: RegisterInfo? registerInfo: RegisterInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>?
) { ) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult( EntrySelectionHelper.startActivityForRegistrationModeResult(
context = activity, context = activity,
activityResultLauncher = activityResultLauncher,
intent = intent, intent = intent,
typeMode = typeMode, 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 androidx.core.graphics.drawable.IconCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase 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.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo 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.getParcelableList
import com.kunzisoft.keepass.utils.putEnumExtra import com.kunzisoft.keepass.utils.putEnumExtra
import com.kunzisoft.keepass.utils.putParcelableList import com.kunzisoft.keepass.utils.putParcelableList
import java.io.IOException
import java.util.UUID import java.util.UUID
object EntrySelectionHelper { object EntrySelectionHelper {
@@ -233,12 +235,26 @@ object EntrySelectionHelper {
removeExtra(EXTRA_NODE_ID) 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 * Intent sender uses special retains data in callback
*/ */
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean { fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
return (specialMode == SpecialMode.SELECTION return (specialMode == SpecialMode.SELECTION
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY)) && (typeMode == TypeMode.MAGIKEYBOARD || typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|| (specialMode == SpecialMode.REGISTRATION || (specialMode == SpecialMode.REGISTRATION
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY)) && (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.AutofillLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
@@ -87,10 +86,6 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
showBlockRestartMessage() showBlockRestartMessage()
autofillLauncherViewModel.cancelResult() autofillLauncherViewModel.cancelResult()
} }
is AutofillLauncherViewModel.UIState.ShowReadOnlyMessage -> {
showReadOnlySaveMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> { is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
showAutofillSuggestionMessage() showAutofillSuggestionMessage()
} }
@@ -101,8 +96,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
// Retrieve the UI // Retrieve the UI
autofillLauncherViewModel.credentialUiState.collect { uiState -> autofillLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) { when (uiState) {
is CredentialLauncherViewModel.UIState.Loading -> {} is CredentialLauncherViewModel.CredentialState.Loading -> {}
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> { is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection( GroupActivity.launchForSelection(
context = this@AutofillLauncherActivity, context = this@AutofillLauncherActivity,
database = uiState.database, database = uiState.database,
@@ -111,7 +106,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
activityResultLauncher = mAutofillSelectionActivityResultLauncher, activityResultLauncher = mAutofillSelectionActivityResultLauncher,
) )
} }
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> { is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration( GroupActivity.launchForRegistration(
context = this@AutofillLauncherActivity, context = this@AutofillLauncherActivity,
database = uiState.database, database = uiState.database,
@@ -120,7 +115,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
activityResultLauncher = mAutofillRegistrationActivityResultLauncher activityResultLauncher = mAutofillRegistrationActivityResultLauncher
) )
} }
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> { is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection( FileDatabaseSelectActivity.launchForSelection(
context = this@AutofillLauncherActivity, context = this@AutofillLauncherActivity,
searchInfo = uiState.searchInfo, searchInfo = uiState.searchInfo,
@@ -128,7 +123,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
activityResultLauncher = mAutofillSelectionActivityResultLauncher activityResultLauncher = mAutofillSelectionActivityResultLauncher
) )
} }
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> { is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration( FileDatabaseSelectActivity.launchForRegistration(
context = this@AutofillLauncherActivity, context = this@AutofillLauncherActivity,
registerInfo = uiState.registerInfo, registerInfo = uiState.registerInfo,
@@ -136,14 +131,14 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
activityResultLauncher = mAutofillRegistrationActivityResultLauncher, activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
) )
} }
is CredentialLauncherViewModel.UIState.SetActivityResult -> { is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult( setActivityResult(
lockDatabase = uiState.lockDatabase, lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode, resultCode = uiState.resultCode,
data = uiState.data data = uiState.data
) )
} }
is CredentialLauncherViewModel.UIState.ShowError -> { is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error) toastError(uiState.error)
autofillLauncherViewModel.cancelResult() autofillLauncherViewModel.cancelResult()
} }
@@ -174,10 +169,6 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
).show() ).show()
} }
private fun showReadOnlySaveMessage() {
toastError(RegisterInReadOnlyDatabaseException())
}
companion object { companion object {
private val TAG = AutofillLauncherActivity::class.java.name 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.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle 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.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity 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.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.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.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 com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
/** /**
* Activity to search or select entry in database, * Activity to search or select entry in database,
@@ -43,201 +45,129 @@ import com.kunzisoft.keepass.view.toastError
*/ */
class EntrySelectionLauncherActivity : DatabaseModeActivity() { class EntrySelectionLauncherActivity : DatabaseModeActivity() {
override fun applyCustomStyle(): Boolean { private val entrySelectionViewModel: EntrySelectionViewModel by viewModels()
return false
}
override fun finishActivityIfReloadRequested(): Boolean { private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
return false 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?) { override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database) super.onUnknownDatabaseRetrieved(database)
entrySelectionViewModel.launchActionIfNeeded(intent, mSpecialMode, 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()
} }
private fun launchSelection(database: ContextualDatabase?, override fun onDestroy() {
sharedWebDomain: String?, super.onDestroy()
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
)
}
}
)
} }
companion object { companion object {
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE" fun launch(
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO" context: Context,
searchInfo: SearchInfo? = null
fun launch(context: Context, ) {
searchInfo: SearchInfo? = null) { context.startActivity(Intent(
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply { context,
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply { EntrySelectionLauncherActivity::class.java
putParcelable(KEY_SEARCH_INFO, searchInfo) ).apply {
}) addSearchInfo(searchInfo)
} // New task needed because don't launch from an Activity context
// New task needed because don't launch from an Activity context flags = Intent.FLAG_ACTIVITY_NEW_TASK or
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
Intent.FLAG_ACTIVITY_CLEAR_TASK })
context.startActivity(intent)
} }
} }
} }

View File

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

View File

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

View File

@@ -462,9 +462,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast) KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
} }
fun performSelection(items: List<EntryInfo>, fun performSelection(
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit, items: List<EntryInfo>,
actionEntrySelection: (autoSearch: Boolean) -> Unit) { actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit
) {
EntrySelectionHelper.performSelection( EntrySelectionHelper.performSelection(
items = items, items = items,
actionPopulateCredentialProvider = { itemFound -> actionPopulateCredentialProvider = { itemFound ->
@@ -478,15 +480,5 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
actionEntrySelection = actionEntrySelection 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.activity.result.ActivityResult
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeNodesIds import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveAndRemoveEntries
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodesIds
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode 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.AutofillHelper.retrieveAutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.ContextualDatabase 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.database.helper.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
@@ -119,7 +118,7 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
onItemNotFound = { openedDatabase -> onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry // Show the database UI to select the entry
mCredentialUiState.value = mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection( CredentialState.LaunchGroupActivityForSelection(
database = openedDatabase, database = openedDatabase,
searchInfo = searchInfo, searchInfo = searchInfo,
typeMode = TypeMode.AUTOFILL typeMode = TypeMode.AUTOFILL
@@ -128,7 +127,7 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
onDatabaseClosed = { onDatabaseClosed = {
// If database not open // If database not open
mCredentialUiState.value = mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection( CredentialState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo, searchInfo = searchInfo,
typeMode = TypeMode.AUTOFILL typeMode = TypeMode.AUTOFILL
) )
@@ -156,17 +155,10 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
Log.d(TAG, "Autofill selection result") Log.d(TAG, "Autofill selection result")
if (intent == null) if (intent == null)
throw IOException("Intent is null") throw IOException("Intent is null")
val nodesIds = intent.retrieveNodesIds() val entries = intent.retrieveAndRemoveEntries(database)
?: throw IOException("NodesIds is null")
intent.removeNodesIds()
val autofillComponent = mAutofillComponent val autofillComponent = mAutofillComponent
if (autofillComponent == null) if (autofillComponent == null)
throw IOException("Autofill component is null") throw IOException("Autofill component is null")
val entries = nodesIds.mapNotNull { nodeId ->
database
.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
AutofillHelper.buildResponse( AutofillHelper.buildResponse(
context = getApplication(), context = getApplication(),
@@ -211,32 +203,36 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
if (!readOnly) { if (!readOnly) {
// Show the database UI to select the entry // Show the database UI to select the entry
mCredentialUiState.value = mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration( CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase, database = openedDatabase,
registerInfo = registerInfo, registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL typeMode = TypeMode.AUTOFILL
) )
} else { } else {
mUiState.value = UIState.ShowReadOnlyMessage mCredentialUiState.value = CredentialState.ShowError(
RegisterInReadOnlyDatabaseException()
)
} }
}, },
onItemNotFound = { openedDatabase -> onItemNotFound = { openedDatabase ->
if (!readOnly) { if (!readOnly) {
// Show the database UI to select the entry // Show the database UI to select the entry
mCredentialUiState.value = mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration( CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase, database = openedDatabase,
registerInfo = registerInfo, registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL typeMode = TypeMode.AUTOFILL
) )
} else { } else {
mUiState.value = UIState.ShowReadOnlyMessage mCredentialUiState.value = CredentialState.ShowError(
RegisterInReadOnlyDatabaseException()
)
} }
}, },
onDatabaseClosed = { onDatabaseClosed = {
// If database not open // If database not open
mCredentialUiState.value = mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration( CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo, registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL typeMode = TypeMode.AUTOFILL
) )
@@ -274,7 +270,6 @@ class AutofillLauncherViewModel(application: Application): CredentialLauncherVie
sealed class UIState { sealed class UIState {
object Loading: UIState() object Loading: UIState()
object ShowBlockRestartMessage: UIState() object ShowBlockRestartMessage: UIState()
object ShowReadOnlyMessage: UIState()
object ShowAutofillSuggestionMessage: UIState() object ShowAutofillSuggestionMessage: UIState()
} }

View File

@@ -25,12 +25,12 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
protected var isResultLauncherRegistered: Boolean = false protected var isResultLauncherRegistered: Boolean = false
private var mSelectionResult: ActivityResult? = null private var mSelectionResult: ActivityResult? = null
protected val mCredentialUiState = MutableStateFlow<UIState>(UIState.Loading) protected val mCredentialUiState = MutableStateFlow<CredentialState>(CredentialState.Loading)
val credentialUiState: StateFlow<UIState> = mCredentialUiState val credentialUiState: StateFlow<CredentialState> = mCredentialUiState
fun showError(error: Throwable) { fun showError(error: Throwable) {
Log.e(TAG, "Error on credential provider launch", error) Log.e(TAG, "Error on credential provider launch", error)
mCredentialUiState.value = UIState.ShowError(error) mCredentialUiState.value = CredentialState.ShowError(error)
} }
open fun onResult() { open fun onResult() {
@@ -41,7 +41,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
fun setResult(intent: Intent, lockDatabase: Boolean = false) { fun setResult(intent: Intent, lockDatabase: Boolean = false) {
// Remove the launcher register // Remove the launcher register
onResult() onResult()
mCredentialUiState.value = UIState.SetActivityResult( mCredentialUiState.value = CredentialState.SetActivityResult(
lockDatabase = lockDatabase, lockDatabase = lockDatabase,
resultCode = RESULT_OK, resultCode = RESULT_OK,
data = intent data = intent
@@ -50,7 +50,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
fun cancelResult(lockDatabase: Boolean = false) { fun cancelResult(lockDatabase: Boolean = false) {
onResult() onResult()
mCredentialUiState.value = UIState.SetActivityResult( mCredentialUiState.value = CredentialState.SetActivityResult(
lockDatabase = lockDatabase, lockDatabase = lockDatabase,
resultCode = RESULT_CANCELED resultCode = RESULT_CANCELED
) )
@@ -115,34 +115,34 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
database: ContextualDatabase? database: ContextualDatabase?
) )
sealed class UIState { sealed class CredentialState {
object Loading : UIState() object Loading : CredentialState()
data class LaunchGroupActivityForSelection( data class LaunchGroupActivityForSelection(
val database: ContextualDatabase, val database: ContextualDatabase,
val searchInfo: SearchInfo?, val searchInfo: SearchInfo?,
val typeMode: TypeMode val typeMode: TypeMode
): UIState() ): CredentialState()
data class LaunchGroupActivityForRegistration( data class LaunchGroupActivityForRegistration(
val database: ContextualDatabase, val database: ContextualDatabase,
val registerInfo: RegisterInfo?, val registerInfo: RegisterInfo?,
val typeMode: TypeMode val typeMode: TypeMode
): UIState() ): CredentialState()
data class LaunchFileDatabaseSelectActivityForSelection( data class LaunchFileDatabaseSelectActivityForSelection(
val searchInfo: SearchInfo?, val searchInfo: SearchInfo?,
val typeMode: TypeMode val typeMode: TypeMode
): UIState() ): CredentialState()
data class LaunchFileDatabaseSelectActivityForRegistration( data class LaunchFileDatabaseSelectActivityForRegistration(
val registerInfo: RegisterInfo?, val registerInfo: RegisterInfo?,
val typeMode: TypeMode val typeMode: TypeMode
): UIState() ): CredentialState()
data class SetActivityResult( data class SetActivityResult(
val lockDatabase: Boolean, val lockDatabase: Boolean,
val resultCode: Int, val resultCode: Int,
val data: Intent? = null val data: Intent? = null
): UIState() ): CredentialState()
data class ShowError( data class ShowError(
val error: Throwable val error: Throwable
): UIState() ): CredentialState()
} }
companion object { 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" "launch manual selection in opened database"
) )
mCredentialUiState.value = mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection( CredentialState.LaunchGroupActivityForSelection(
database = openedDatabase, database = openedDatabase,
searchInfo = searchInfo, searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY typeMode = TypeMode.PASSKEY
@@ -247,7 +247,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database") Log.d(TAG, "Manual passkey selection in closed database")
mCredentialUiState.value = mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection( CredentialState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo, searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY typeMode = TypeMode.PASSKEY
) )
@@ -426,7 +426,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
"but launch manual registration for a new entry" "but launch manual registration for a new entry"
) )
mCredentialUiState.value = mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration( CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase, database = openedDatabase,
registerInfo = registerInfo, registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY typeMode = TypeMode.PASSKEY
@@ -435,7 +435,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
onItemNotFound = { openedDatabase -> onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database") Log.d(TAG, "Launch new manual registration in opened database")
mCredentialUiState.value = mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration( CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase, database = openedDatabase,
registerInfo = registerInfo, registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY typeMode = TypeMode.PASSKEY
@@ -444,7 +444,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Manual passkey registration in closed database") Log.d(TAG, "Manual passkey registration in closed database")
mCredentialUiState.value = mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration( CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo, registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY typeMode = TypeMode.PASSKEY
) )