mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Magikeyboard Auto search #2233
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
entrySelectionViewModel.manageSelectionResult(it)
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return false
|
||||
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)
|
||||
})
|
||||
}
|
||||
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
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -462,9 +462,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
||||
}
|
||||
|
||||
fun performSelection(items: List<EntryInfo>,
|
||||
fun performSelection(
|
||||
items: List<EntryInfo>,
|
||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||
actionEntrySelection: (autoSearch: Boolean) -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user