fix: Autofill refactoring

This commit is contained in:
J-Jamet
2025-09-26 21:42:22 +02:00
parent 76c20263f7
commit dd0d85e54e
22 changed files with 1008 additions and 620 deletions

View File

@@ -56,9 +56,10 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
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.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService 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
@@ -203,8 +204,8 @@ class EntryEditActivity : DatabaseLockActivity(),
mDatabase, mDatabase,
entryId, entryId,
parentId, parentId,
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent) intent.retrieveRegisterInfo()
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)?.toRegisterInfo() ?: intent.retrieveSearchInfo()?.toRegisterInfo()
) )
// To retrieve attachment // To retrieve attachment
@@ -378,7 +379,7 @@ class EntryEditActivity : DatabaseLockActivity(),
intent = intent, intent = intent,
defaultAction = {}, defaultAction = {},
searchAction = {}, searchAction = {},
selectionAction = { intentSender, typeMode, searchInfo, autofillComponent -> selectionAction = { intentSender, typeMode, searchInfo ->
when(typeMode) { when(typeMode) {
TypeMode.DEFAULT -> {} TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD -> TypeMode.MAGIKEYBOARD ->
@@ -396,7 +397,7 @@ class EntryEditActivity : DatabaseLockActivity(),
TypeMode.PASSKEY -> TypeMode.PASSKEY ->
entryValidatedForPasskeyRegistration(database, entrySave.newEntry) entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
TypeMode.AUTOFILL -> TypeMode.AUTOFILL ->
entryValidatedForAutofillRegistration(entrySave.newEntry) entryValidatedForAutofillRegistration(database, entrySave.newEntry)
} }
} }
) )
@@ -444,7 +445,7 @@ class EntryEditActivity : DatabaseLockActivity(),
searchAction = { searchAction = {
// Nothing when search retrieved // Nothing when search retrieved
}, },
selectionAction = { intentSender, typeMode, searchInfo, autofillComponent -> selectionAction = { intentSender, typeMode, searchInfo ->
when(typeMode) { when(typeMode) {
TypeMode.DEFAULT -> {} TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD -> TypeMode.MAGIKEYBOARD ->
@@ -463,7 +464,7 @@ class EntryEditActivity : DatabaseLockActivity(),
TypeMode.PASSKEY -> TypeMode.PASSKEY ->
entryValidatedForPasskeyRegistration(database, entry) entryValidatedForPasskeyRegistration(database, entry)
TypeMode.AUTOFILL -> TypeMode.AUTOFILL ->
entryValidatedForAutofillRegistration(entry) entryValidatedForAutofillRegistration(database, entry)
} }
} }
) )
@@ -496,9 +497,10 @@ class EntryEditActivity : DatabaseLockActivity(),
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) { private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
// Build Autofill response with the entry selected // Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity, this.buildSpecialModeResponseAndSetResult(
database, entryInfo = entry.getEntryInfo(database),
entry.getEntryInfo(database)) extras = buildEntryResult(entry)
)
} }
onValidateSpecialMode() onValidateSpecialMode()
} }
@@ -506,20 +508,21 @@ class EntryEditActivity : DatabaseLockActivity(),
private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) { private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
this.buildPasskeyResponseAndSetResult( this.buildPasskeyResponseAndSetResult(
entryInfo = entry.getEntryInfo(database) entryInfo = entry.getEntryInfo(database),
extras = buildEntryResult(entry)
) )
} }
onValidateSpecialMode() onValidateSpecialMode()
} }
private fun entryValidatedForAutofillRegistration(entry: Entry) { private fun entryValidatedForAutofillRegistration(database: ContextualDatabase, entry: Entry) {
//if (isIntentSender()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// TODO Autofill Callback #765 this.buildSpecialModeResponseAndSetResult(
//} entryInfo = entry.getEntryInfo(database),
onValidateSpecialMode() extras = buildEntryResult(entry)
if (!isIntentSender()) { )
finishForEntryResult(entry)
} }
onValidateSpecialMode()
} }
private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) { private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) {
@@ -856,7 +859,6 @@ class EntryEditActivity : DatabaseLockActivity(),
typeMode: TypeMode, typeMode: TypeMode,
groupId: NodeId<*>, groupId: NodeId<*>,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
autofillComponent: AutofillComponent? = null,
activityResultLauncher: ActivityResultLauncher<Intent>? = null, activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) { ) {
if (database.loaded && !database.isReadOnly) { if (database.loaded && !database.isReadOnly) {
@@ -868,7 +870,6 @@ class EntryEditActivity : DatabaseLockActivity(),
intent = intent, intent = intent,
typeMode = typeMode, typeMode = typeMode,
searchInfo = searchInfo, searchInfo = searchInfo,
autofillComponent = autofillComponent,
activityResultLauncher = activityResultLauncher activityResultLauncher = activityResultLauncher
) )
} }

View File

@@ -50,7 +50,6 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
@@ -303,7 +302,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
) )
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
selectionAction = { intentSenderMode, typeMode, searchInfo, autofillComponent -> selectionAction = { intentSenderMode, typeMode, searchInfo ->
MainCredentialActivity.launchForSelection( MainCredentialActivity.launchForSelection(
activity = this, activity = this,
activityResultLauncher = if (intentSenderMode) activityResultLauncher = if (intentSenderMode)
@@ -312,8 +311,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
keyFile = keyFile, keyFile = keyFile,
hardwareKey = hardwareKey, hardwareKey = hardwareKey,
typeMode = typeMode, typeMode = typeMode,
searchInfo = searchInfo, searchInfo = searchInfo
autofillComponent = autofillComponent,
) )
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
@@ -496,18 +494,16 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
*/ */
fun launchForSelection( fun launchForSelection(
activity: Activity, context: Context,
typeMode: TypeMode, typeMode: TypeMode,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
autofillComponent: AutofillComponent? = null,
activityResultLauncher: ActivityResultLauncher<Intent>? = null, activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) { ) {
EntrySelectionHelper.startActivityForSelectionModeResult( EntrySelectionHelper.startActivityForSelectionModeResult(
context = activity, context = context,
intent = Intent(activity, FileDatabaseSelectActivity::class.java), intent = Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo = searchInfo, searchInfo = searchInfo,
typeMode = typeMode, typeMode = typeMode,
autofillComponent = autofillComponent,
activityResultLauncher = activityResultLauncher activityResultLauncher = activityResultLauncher
) )
} }

View File

@@ -64,10 +64,13 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService 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
@@ -495,14 +498,13 @@ class GroupActivity : DatabaseLockActivity(),
searchAction = { searchAction = {
// Search not used // Search not used
}, },
selectionAction = { intentSenderMode, typeMode, searchInfo, autofillComponent -> selectionAction = { intentSenderMode, typeMode, searchInfo ->
EntryEditActivity.launchForSelection( EntryEditActivity.launchForSelection(
context = this@GroupActivity, context = this@GroupActivity,
database = database, database = database,
typeMode = typeMode, typeMode = typeMode,
groupId = currentGroup.nodeId, groupId = currentGroup.nodeId,
searchInfo = searchInfo, searchInfo = searchInfo,
autofillComponent = autofillComponent,
activityResultLauncher = if (intentSenderMode) activityResultLauncher = if (intentSenderMode)
mCredentialActivityResultLauncher else null mCredentialActivityResultLauncher else null
) )
@@ -666,7 +668,7 @@ class GroupActivity : DatabaseLockActivity(),
searchAction = { searchAction = {
// Search not used // Search not used
}, },
selectionAction = { intentSenderMode, typeMode, searchInfo, autofillComponent -> selectionAction = { intentSenderMode, typeMode, searchInfo ->
when (typeMode) { when (typeMode) {
TypeMode.DEFAULT -> {} TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD -> entry?.let { TypeMode.MAGIKEYBOARD -> entry?.let {
@@ -700,7 +702,7 @@ class GroupActivity : DatabaseLockActivity(),
*/ */
private fun transformSearchInfoIntent(intent: Intent) { private fun transformSearchInfoIntent(intent: Intent) {
// To relaunch the activity as ACTION_SEARCH // To relaunch the activity as ACTION_SEARCH
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false) val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
intent.removeExtra(AUTO_SEARCH_KEY) intent.removeExtra(AUTO_SEARCH_KEY)
if (searchInfo != null && autoSearch) { if (searchInfo != null && autoSearch) {
@@ -840,7 +842,7 @@ class GroupActivity : DatabaseLockActivity(),
searchAction = { searchAction = {
// Nothing here, a search is simply performed // Nothing here, a search is simply performed
}, },
selectionAction = { intentSenderMode, typeMode, searchInfo, autofillComponent -> selectionAction = { intentSenderMode, typeMode, searchInfo ->
when (typeMode) { when (typeMode) {
TypeMode.DEFAULT -> {} TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD -> { TypeMode.MAGIKEYBOARD -> {
@@ -910,11 +912,7 @@ class GroupActivity : DatabaseLockActivity(),
removeSearch() removeSearch()
// Build response with the entry selected // Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.buildResponseAndSetResult( this.buildSpecialModeResponseAndSetResult(entry.getEntryInfo(database))
this,
database,
entry.getEntryInfo(database)
)
} }
onValidateSpecialMode() onValidateSpecialMode()
} }
@@ -1381,8 +1379,8 @@ class GroupActivity : DatabaseLockActivity(),
// Else in root, lock if needed // Else in root, lock if needed
else { else {
removeSearch() removeSearch()
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) { if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
lockAndExit() lockAndExit()
} else { } else {
@@ -1513,10 +1511,7 @@ class GroupActivity : DatabaseLockActivity(),
if (database.loaded) { if (database.loaded) {
checkTimeAndBuildIntent(context, null) { intent -> checkTimeAndBuildIntent(context, null) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch) intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
EntrySelectionHelper.addSearchInfoInIntent( intent.addSearchInfo(searchInfo)
intent,
searchInfo
)
context.startActivity(intent) context.startActivity(intent)
} }
} }
@@ -1528,7 +1523,6 @@ class GroupActivity : DatabaseLockActivity(),
typeMode: TypeMode, typeMode: TypeMode,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
autoSearch: Boolean = false, autoSearch: Boolean = false,
autofillComponent: AutofillComponent? = null,
activityResultLauncher: ActivityResultLauncher<Intent>? = null, activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) { ) {
if (database.loaded) { if (database.loaded) {
@@ -1539,7 +1533,6 @@ class GroupActivity : DatabaseLockActivity(),
intent = intent, intent = intent,
typeMode = typeMode, typeMode = typeMode,
searchInfo = searchInfo, searchInfo = searchInfo,
autofillComponent = autofillComponent,
activityResultLauncher = activityResultLauncher activityResultLauncher = activityResultLauncher
) )
} }
@@ -1608,7 +1601,7 @@ class GroupActivity : DatabaseLockActivity(),
onCancelSpecialMode() onCancelSpecialMode()
} }
}, },
selectionAction = { intentSenderMode, typeMode, searchInfo, autofillComponent -> selectionAction = { intentSenderMode, typeMode, searchInfo ->
SearchHelper.checkAutoSearchInfo( SearchHelper.checkAutoSearchInfo(
context = activity, context = activity,
database = database, database = database,
@@ -1644,7 +1637,9 @@ class GroupActivity : DatabaseLockActivity(),
EntrySelectionHelper.performSelection( EntrySelectionHelper.performSelection(
items = items, items = items,
actionPopulateCredentialProvider = { entryInfo -> actionPopulateCredentialProvider = { entryInfo ->
activity.buildPasskeyResponseAndSetResult(entryInfo) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
activity.buildPasskeyResponseAndSetResult(entryInfo)
}
onValidateSpecialMode() onValidateSpecialMode()
}, },
actionEntrySelection = { actionEntrySelection = {
@@ -1662,7 +1657,7 @@ class GroupActivity : DatabaseLockActivity(),
} }
TypeMode.AUTOFILL -> { TypeMode.AUTOFILL -> {
// Response is build // Response is build
AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items) activity.buildSpecialModeResponseAndSetResult(items)
onValidateSpecialMode() onValidateSpecialMode()
} }
} }
@@ -1675,7 +1670,6 @@ class GroupActivity : DatabaseLockActivity(),
typeMode = typeMode, typeMode = typeMode,
searchInfo = searchInfo, searchInfo = searchInfo,
autoSearch = false, autoSearch = false,
autofillComponent = autofillComponent,
activityResultLauncher = if (intentSenderMode) activityResultLauncher = if (intentSenderMode)
activityResultLauncher else null activityResultLauncher else null
) )

View File

@@ -57,7 +57,6 @@ import com.kunzisoft.keepass.biometric.deviceUnlockError
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -815,7 +814,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
hardwareKey: HardwareKey?, hardwareKey: HardwareKey?,
typeMode: TypeMode, typeMode: TypeMode,
searchInfo: SearchInfo?, searchInfo: SearchInfo?,
autofillComponent: AutofillComponent? = null,
activityResultLauncher: ActivityResultLauncher<Intent>? = null, activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) { ) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
@@ -824,7 +822,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
intent = intent, intent = intent,
typeMode = typeMode, typeMode = typeMode,
searchInfo = searchInfo, searchInfo = searchInfo,
autofillComponent = autofillComponent,
activityResultLauncher = activityResultLauncher activityResultLauncher = activityResultLauncher
) )
} }

View File

@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.adapters.NodesAdapter import com.kunzisoft.keepass.adapters.NodesAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.SortNodeEnum
@@ -248,7 +248,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener) mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
activity?.intent?.let { activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it) specialMode = it.retrieveSpecialMode()
} }
} }

View File

@@ -34,7 +34,7 @@ import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
@@ -395,7 +395,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
// If in registration mode, don't allow read only // If in registration mode, don't allow read only
if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) { if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
finish() finish()
} }
} }

View File

@@ -7,9 +7,14 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
@@ -56,8 +61,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
fun onLaunchActivitySpecialMode() { fun onLaunchActivitySpecialMode() {
if (!isIntentSender()) { if (!isIntentSender()) {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
finish() finish()
} }
} }
@@ -66,8 +71,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
if (isIntentSender()) { if (isIntentSender()) {
super.finish() super.finish()
} else { } else {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish() backToTheMainAppAndFinish()
} }
@@ -79,8 +84,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
// To get the app caller, only for IntentSender // To get the app caller, only for IntentSender
onRegularBackPressed() onRegularBackPressed()
} else { } else {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish() backToTheMainAppAndFinish()
} }
@@ -111,18 +116,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
} }
}) })
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent) mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent) mTypeMode = intent.retrieveTypeMode()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent) mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent) mTypeMode = intent.retrieveTypeMode()
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent) val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
val searchInfo: SearchInfo? = registerInfo?.searchInfo val searchInfo: SearchInfo? = registerInfo?.searchInfo
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) ?: intent.retrieveSearchInfo()
// To show the selection mode // To show the selection mode
mToolbarSpecial = findViewById(R.id.special_mode_view) mToolbarSpecial = findViewById(R.id.special_mode_view)

View File

@@ -24,6 +24,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.util.Log import android.util.Log
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@@ -32,9 +34,6 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
@@ -43,7 +42,10 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.getEnumExtra import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
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 java.util.UUID
object EntrySelectionHelper { object EntrySelectionHelper {
@@ -51,6 +53,8 @@ object EntrySelectionHelper {
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE" private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO" private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO" private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
private const val EXTRA_NODES_IDS = "com.kunzisoft.keepass.extra.NODES_IDS"
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.NODE_ID"
/** /**
* Finish the activity by passing the result code and by locking the database if necessary * Finish the activity by passing the result code and by locking the database if necessary
@@ -107,8 +111,8 @@ object EntrySelectionHelper {
intent: Intent, intent: Intent,
searchInfo: SearchInfo searchInfo: SearchInfo
) { ) {
addSpecialModeInIntent(intent, SpecialMode.SEARCH) intent.addSpecialMode(SpecialMode.SEARCH)
addSearchInfoInIntent(intent, searchInfo) intent.addSearchInfo(searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent) context.startActivity(intent)
} }
@@ -118,17 +122,11 @@ object EntrySelectionHelper {
intent: Intent, intent: Intent,
typeMode: TypeMode, typeMode: TypeMode,
searchInfo: SearchInfo?, searchInfo: SearchInfo?,
autofillComponent: AutofillComponent? = null,
activityResultLauncher: ActivityResultLauncher<Intent>? = null, activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) { ) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION) intent.addSpecialMode(SpecialMode.SELECTION)
addTypeModeInIntent(intent, typeMode) intent.addTypeMode(typeMode)
addSearchInfoInIntent(intent, searchInfo) intent.addSearchInfo(searchInfo)
autofillComponent?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.addAutofillComponent(context, autofillComponent)
}
}
if (activityResultLauncher == null) { if (activityResultLauncher == null) {
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
@@ -142,77 +140,123 @@ object EntrySelectionHelper {
registerInfo: RegisterInfo?, registerInfo: RegisterInfo?,
typeMode: TypeMode typeMode: TypeMode
) { ) {
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION) intent.addSpecialMode(SpecialMode.REGISTRATION)
addTypeModeInIntent(intent, typeMode) intent.addTypeMode(typeMode)
addRegisterInfoInIntent(intent, registerInfo) intent.addRegisterInfo(registerInfo)
if (activityResultLauncher == null) { if (activityResultLauncher == null) {
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
activityResultLauncher?.launch(intent) ?: context.startActivity(intent) activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
} }
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) { /**
* Build the special mode response for internal entry selection for one entry
*/
fun Activity.buildSpecialModeResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
this.buildSpecialModeResponseAndSetResult(listOf(entryInfo), extras)
}
/**
* Build the special mode response for internal entry selection for multiple entries
*/
fun Activity.buildSpecialModeResponseAndSetResult(
entriesInfo: List<EntryInfo>,
extras: Bundle? = null
) {
try {
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success special mode manual selection")
mReplyIntent.addNodesIds(entriesInfo.map { it.id })
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the result", e)
setResult(Activity.RESULT_CANCELED)
}
}
fun Intent.addSearchInfo(searchInfo: SearchInfo?): Intent {
searchInfo?.let { searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it) putExtra(KEY_SEARCH_INFO, it)
} }
return this
} }
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? { fun Intent.retrieveSearchInfo(): SearchInfo? {
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO) return getParcelableExtraCompat(KEY_SEARCH_INFO)
} }
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) { fun Intent.addRegisterInfo(registerInfo: RegisterInfo?): Intent {
registerInfo?.let { registerInfo?.let {
intent.putExtra(KEY_REGISTER_INFO, it) putExtra(KEY_REGISTER_INFO, it)
} }
return this
} }
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? { fun Intent.retrieveRegisterInfo(): RegisterInfo? {
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO) return getParcelableExtraCompat(KEY_REGISTER_INFO)
} }
fun removeInfoFromIntent(intent: Intent) { fun Intent.removeInfo() {
intent.removeExtra(KEY_SEARCH_INFO) removeExtra(KEY_SEARCH_INFO)
intent.removeExtra(KEY_REGISTER_INFO) removeExtra(KEY_REGISTER_INFO)
} }
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
// TODO Replace by Intent.addSpecialMode
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
}
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent { fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode) this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
return this return this
} }
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode { fun Intent.retrieveSpecialMode(): SpecialMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return SpecialMode.SELECTION
}
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
} }
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
// TODO Replace by Intent.addTypeMode
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
}
fun Intent.addTypeMode(typeMode: TypeMode): Intent { fun Intent.addTypeMode(typeMode: TypeMode): Intent {
this.putEnumExtra(KEY_TYPE_MODE, typeMode) this.putEnumExtra(KEY_TYPE_MODE, typeMode)
return this return this
} }
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode { fun Intent.retrieveTypeMode(): TypeMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return TypeMode.AUTOFILL
}
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
} }
fun removeModesFromIntent(intent: Intent) { fun Intent.removeModes() {
intent.removeExtra(KEY_SPECIAL_MODE) removeExtra(KEY_SPECIAL_MODE)
intent.removeExtra(KEY_TYPE_MODE) removeExtra(KEY_TYPE_MODE)
}
fun Intent.addNodesIds(nodesIds: List<UUID>): Intent {
this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
return this
}
fun Intent.retrieveNodesIds(): List<UUID>? {
return getParcelableList<ParcelUuid>(EXTRA_NODES_IDS)?.map { it.uuid }
}
fun Intent.removeNodesIds() {
removeExtra(EXTRA_NODES_IDS)
}
/**
* Add the node id to the intent
*/
fun Intent.addNodeId(nodeId: UUID?) {
nodeId?.let {
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
}
}
/**
* Retrieve the node id from the intent
*/
fun Intent.retrieveNodeId(): UUID? {
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
} }
/** /**
@@ -221,9 +265,8 @@ object EntrySelectionHelper {
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.AUTOFILL || typeMode == TypeMode.PASSKEY))
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|| (specialMode == SpecialMode.REGISTRATION || (specialMode == SpecialMode.REGISTRATION
&& typeMode == TypeMode.PASSKEY) && (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
} }
fun doSpecialAction( fun doSpecialAction(
@@ -233,8 +276,7 @@ object EntrySelectionHelper {
selectionAction: ( selectionAction: (
intentSenderMode: Boolean, intentSenderMode: Boolean,
typeMode: TypeMode, typeMode: TypeMode,
searchInfo: SearchInfo?, searchInfo: SearchInfo?
autofillComponent: AutofillComponent?
) -> Unit, ) -> Unit,
registrationAction: ( registrationAction: (
intentSenderMode: Boolean, intentSenderMode: Boolean,
@@ -242,16 +284,16 @@ object EntrySelectionHelper {
registerInfo: RegisterInfo? registerInfo: RegisterInfo?
) -> Unit ) -> Unit
) { ) {
when (val specialMode = retrieveSpecialModeFromIntent(intent)) { when (val specialMode = intent.retrieveSpecialMode()) {
SpecialMode.DEFAULT -> { SpecialMode.DEFAULT -> {
removeModesFromIntent(intent) intent.removeModes()
removeInfoFromIntent(intent) intent.removeInfo()
defaultAction.invoke() defaultAction.invoke()
} }
SpecialMode.SEARCH -> { SpecialMode.SEARCH -> {
val searchInfo = retrieveSearchInfoFromIntent(intent) val searchInfo = intent.retrieveSearchInfo()
removeModesFromIntent(intent) intent.removeModes()
removeInfoFromIntent(intent) intent.removeInfo()
if (searchInfo != null) if (searchInfo != null)
searchAction.invoke(searchInfo) searchAction.invoke(searchInfo)
else { else {
@@ -259,66 +301,55 @@ object EntrySelectionHelper {
} }
} }
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent) val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
var autofillComponentInit = false if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { when (val typeMode = intent.retrieveTypeMode()) {
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent -> TypeMode.DEFAULT -> {
selectionAction.invoke( intent.removeModes()
isIntentSenderMode(specialMode, TypeMode.AUTOFILL), if (searchInfo != null)
TypeMode.AUTOFILL, searchAction.invoke(searchInfo)
searchInfo, else
autofillComponent defaultAction.invoke()
) }
autofillComponentInit = true TypeMode.MAGIKEYBOARD -> selectionAction.invoke(
} isIntentSenderMode(specialMode, typeMode),
} typeMode,
if (!autofillComponentInit) { searchInfo
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) { )
when (val typeMode = retrieveTypeModeFromIntent(intent)) { TypeMode.PASSKEY ->
TypeMode.DEFAULT -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
removeModesFromIntent(intent) selectionAction.invoke(
if (searchInfo != null) isIntentSenderMode(specialMode, typeMode),
searchAction.invoke(searchInfo) typeMode,
else searchInfo
defaultAction.invoke() )
} } else
TypeMode.MAGIKEYBOARD -> selectionAction.invoke( defaultAction.invoke()
isIntentSenderMode(specialMode, typeMode), TypeMode.AUTOFILL -> {
typeMode, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
searchInfo, selectionAction.invoke(
null isIntentSenderMode(specialMode, typeMode),
) typeMode,
TypeMode.PASSKEY -> searchInfo
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { )
selectionAction.invoke( } else
isIntentSenderMode(specialMode, typeMode), defaultAction.invoke()
typeMode,
searchInfo,
null
)
} else
defaultAction.invoke()
else -> {
// In this case, error
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
}
} }
} else {
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
} }
} else {
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
} }
} }
SpecialMode.REGISTRATION -> { SpecialMode.REGISTRATION -> {
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent) val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
val typeMode = retrieveTypeModeFromIntent(intent) val typeMode = intent.retrieveTypeMode()
val intentSenderMode = isIntentSenderMode(specialMode, typeMode) val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
if (!intentSenderMode) { if (!intentSenderMode) {
removeModesFromIntent(intent) intent.removeModes()
removeInfoFromIntent(intent) intent.removeInfo()
} }
if (registerInfo != null) if (registerInfo != null)
registrationAction.invoke( registrationAction.invoke(

View File

@@ -27,34 +27,46 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
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.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService 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.database.exception.RegisterInReadOnlyDatabaseException
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
import com.kunzisoft.keepass.utils.AppUtil.getConcreteWebDomain import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() { class AutofillLauncherActivity : DatabaseModeActivity() {
override var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? = private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
this.buildActivityResultLauncher(typeMode = TypeMode.AUTOFILL, lockDatabase = true)
private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageSelectionResult(it)
}
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageRegistrationResult(it)
}
override fun applyCustomStyle(): Boolean { override fun applyCustomStyle(): Boolean {
return false return false
@@ -64,176 +76,103 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
return true return true
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onDatabaseRetrieved(database) super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Retrieve selection mode // Retrieve the UI
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode -> autofillLauncherViewModel.credentialUiState.collect { uiState ->
when (specialMode) { when (uiState) {
SpecialMode.SELECTION -> { is CredentialLauncherViewModel.UIState.Loading -> {}
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle -> is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
// To pass extra inline request GroupActivity.launchForSelection(
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null context = this@AutofillLauncherActivity,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { database = uiState.database,
compatInlineSuggestionsRequest = bundle.getParcelableCompat( searchInfo = uiState.searchInfo,
KEY_INLINE_SUGGESTION typeMode = uiState.typeMode,
) activityResultLauncher = mAutofillSelectionActivityResultLauncher,
} )
// Build search param
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
searchInfo.getConcreteWebDomain(this) { concreteWebDomain ->
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper
.retrieveAutofillComponent(intent)
?.assistStructure
val newAutofillComponent = if (assistStructure != null) {
AutofillComponent(
assistStructure,
compatInlineSuggestionsRequest
)
} else {
null
}
searchInfo.webDomain = concreteWebDomain
launchSelection(database, newAutofillComponent, searchInfo)
}
}
} }
// Remove bundle is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
intent.removeExtra(KEY_SELECTION_BUNDLE) GroupActivity.launchForRegistration(
} context = this@AutofillLauncherActivity,
SpecialMode.REGISTRATION -> { database = uiState.database,
// To register info registerInfo = uiState.registerInfo,
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>( typeMode = uiState.typeMode,
KEY_REGISTER_INFO activityResultLauncher = mAutofillRegistrationActivityResultLauncher
) )
val searchInfo = SearchInfo(registerInfo?.searchInfo) }
searchInfo.getConcreteWebDomain(this) { concreteWebDomain -> is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
searchInfo.webDomain = concreteWebDomain FileDatabaseSelectActivity.launchForSelection(
launchRegistration(database, searchInfo, registerInfo) context = this@AutofillLauncherActivity,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillSelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@AutofillLauncherActivity,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
)
}
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
setActivityResult(
typeMode = TypeMode.AUTOFILL,
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.UIState.ShowError -> {
toastError(uiState.error)
autofillLauncherViewModel.cancelResult()
} }
} }
else -> { }
// Not an autofill call }
setResult(RESULT_CANCELED) lifecycleScope.launch {
finish() // Initialize the parameters
autofillLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
AutofillLauncherViewModel.UIState.Loading -> {}
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
showBlockRestartMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowReadOnlyMessage -> {
showReadOnlySaveMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
showAutofillSuggestionMessage()
}
} }
} }
} }
} }
private fun launchSelection(database: ContextualDatabase?, override fun onDatabaseRetrieved(database: ContextualDatabase?) {
autofillComponent: AutofillComponent?, super.onDatabaseRetrieved(database)
searchInfo: SearchInfo) { autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
if (autofillComponent == null) {
setResult(RESULT_CANCELED)
finish()
} else if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = this
)) {
// If database is open
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Items found
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
finish()
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
GroupActivity.launchForSelection(
context = this,
database = openedDatabase,
typeMode = TypeMode.AUTOFILL,
searchInfo = searchInfo,
autoSearch = false,
autofillComponent = autofillComponent,
activityResultLauncher = mCredentialActivityResultLauncher,
)
},
onDatabaseClosed = {
// If database not open
FileDatabaseSelectActivity.launchForSelection(
activity = this,
typeMode = TypeMode.AUTOFILL,
searchInfo = searchInfo,
autofillComponent = autofillComponent,
activityResultLauncher = mCredentialActivityResultLauncher
)
}
)
} else {
showBlockRestartMessage()
setResult(RESULT_CANCELED)
finish()
}
}
private fun launchRegistration(database: ContextualDatabase?,
searchInfo: SearchInfo,
registerInfo: RegisterInfo?) {
if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = this
)) {
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, _ ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = null, // TODO Autofill result launcher #765
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
showReadOnlySaveMessage()
}
},
onItemNotFound = { openedDatabase ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = null, // TODO Autofill result launcher #765
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
showReadOnlySaveMessage()
}
},
onDatabaseClosed = {
// If database not open
FileDatabaseSelectActivity.launchForRegistration(
context = this,
activityResultLauncher = null, // TODO Autofill result launcher #765
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
}
)
} else {
showBlockRestartMessage()
setResult(RESULT_CANCELED)
}
finish()
} }
private fun showBlockRestartMessage() { private fun showBlockRestartMessage() {
// If item not allowed, show a toast // If item not allowed, show a toast
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show() Toast.makeText(
applicationContext,
R.string.autofill_block_restart,
Toast.LENGTH_LONG
).show()
}
private fun showAutofillSuggestionMessage() {
Toast.makeText(
applicationContext,
R.string.autofill_inline_suggestions_keyboard,
Toast.LENGTH_SHORT
).show()
} }
private fun showReadOnlySaveMessage() { private fun showReadOnlySaveMessage() {
@@ -244,27 +183,21 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
private val TAG = AutofillLauncherActivity::class.java.name private val TAG = AutofillLauncherActivity::class.java.name
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE" fun getPendingIntentForSelection(
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO" context: Context,
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION" searchInfo: SearchInfo? = null,
autofillComponent: AutofillComponent
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" ): PendingIntent? {
fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? {
try { try {
return PendingIntent.getActivity( return PendingIntent.getActivity(
context, 0, context,
randomRequestCode(),
// Doesn't work with direct extra Parcelable (don't know why?) // Doesn't work with direct extra Parcelable (don't know why?)
// Wrap into a bundle to bypass the problem // Wrap into a bundle to bypass the problem
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply { addSpecialMode(SpecialMode.SELECTION)
putParcelable(KEY_SEARCH_INFO, searchInfo) addSearchInfo(searchInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { addAutofillComponent(autofillComponent)
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
}
})
}, },
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
@@ -278,14 +211,17 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
} }
} }
fun getPendingIntentForRegistration(context: Context, fun getPendingIntentForRegistration(
registerInfo: RegisterInfo): PendingIntent? { context: Context,
registerInfo: RegisterInfo
): PendingIntent? {
try { try {
return PendingIntent.getActivity( return PendingIntent.getActivity(
context, 0, context,
randomRequestCode(),
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) addSpecialMode(SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo) addRegisterInfo(registerInfo)
}, },
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
@@ -299,11 +235,13 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
} }
} }
fun launchForRegistration(context: Context, fun launchForRegistration(
registerInfo: RegisterInfo) { context: Context,
registerInfo: RegisterInfo
) {
val intent = Intent(context, AutofillLauncherActivity::class.java) val intent = Intent(context, AutofillLauncherActivity::class.java)
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION) intent.addSpecialMode(SpecialMode.REGISTRATION)
intent.putExtra(KEY_REGISTER_INFO, registerInfo) intent.addRegisterInfo(registerInfo)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent) context.startActivity(intent)
} }

View File

@@ -105,12 +105,11 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
sharedWebDomain: String?, sharedWebDomain: String?,
otpString: String?) { otpString: String?) {
// Build domain search param // Build domain search param
val searchInfo = SearchInfo().apply { getConcreteWebDomain(this, sharedWebDomain) { concreteWebDomain ->
this.webDomain = sharedWebDomain val searchInfo = SearchInfo().apply {
this.otpString = otpString this.webDomain = concreteWebDomain
} this.otpString = otpString
searchInfo.getConcreteWebDomain(this) { concreteWebDomain -> }
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo) launch(database, searchInfo)
} }
} }
@@ -212,7 +211,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
) )
} else if (searchShareForMagikeyboard) { } else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForSelection( FileDatabaseSelectActivity.launchForSelection(
activity = this, context = this,
typeMode = TypeMode.MAGIKEYBOARD, typeMode = TypeMode.MAGIKEYBOARD,
searchInfo = searchInfo searchInfo = searchInfo
) )

View File

@@ -36,6 +36,8 @@ import com.kunzisoft.keepass.R
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.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
@@ -44,14 +46,13 @@ import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
@@ -121,10 +122,9 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
GroupActivity.launchForSelection( GroupActivity.launchForSelection(
context = this@PasskeyLauncherActivity, context = this@PasskeyLauncherActivity,
database = uiState.database, database = uiState.database,
typeMode = TypeMode.PASSKEY, typeMode = uiState.typeMode,
activityResultLauncher = mPasskeySelectionActivityResultLauncher, activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = null, searchInfo = uiState.searchInfo
autoSearch = false
) )
} }
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> { is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
@@ -138,8 +138,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
} }
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> { is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection( FileDatabaseSelectActivity.launchForSelection(
activity = this@PasskeyLauncherActivity, context = this@PasskeyLauncherActivity,
typeMode = TypeMode.PASSKEY, typeMode = uiState.typeMode,
activityResultLauncher = mPasskeySelectionActivityResultLauncher, activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = uiState.searchInfo, searchInfo = uiState.searchInfo,
) )
@@ -276,7 +276,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
): PendingIntent? { ): PendingIntent? {
return PendingIntent.getActivity( return PendingIntent.getActivity(
context, context,
(Math.random() * Integer.MAX_VALUE).toInt(), randomRequestCode(),
Intent(context, PasskeyLauncherActivity::class.java).apply { Intent(context, PasskeyLauncherActivity::class.java).apply {
addSpecialMode(specialMode) addSpecialMode(specialMode)
addTypeMode(TypeMode.PASSKEY) addTypeMode(TypeMode.PASSKEY)

View File

@@ -2,5 +2,7 @@ package com.kunzisoft.keepass.credentialprovider.autofill
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
data class AutofillComponent(val assistStructure: AssistStructure, data class AutofillComponent(
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?) val assistStructure: AssistStructure,
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?
)

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.credentialprovider.autofill package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent import android.app.PendingIntent
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.content.Context import android.content.Context
@@ -38,7 +37,6 @@ import android.view.autofill.AutofillId
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast
import android.widget.inline.InlinePresentationSpec import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions import androidx.autofill.inline.UiVersions
@@ -54,21 +52,32 @@ import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.AppUtil.getConcreteWebDomain
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import java.io.IOException
import kotlin.math.min import kotlin.math.min
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
object AutofillHelper { object AutofillHelper {
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE private const val EXTRA_BASE_STRUCTURE = "com.kunzisoft.keepass.autofill.BASE_STRUCTURE"
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST" private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? { fun Intent.addAutofillComponent(autofillComponent: AutofillComponent) {
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure -> this.putExtra(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
autofillComponent.compatInlineSuggestionsRequest?.let {
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
}
fun Intent.retrieveAutofillComponent(): AutofillComponent? {
getParcelableExtraCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AutofillComponent(assistStructure, AutofillComponent(assistStructure,
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)) getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
} else { } else {
AutofillComponent(assistStructure, null) AutofillComponent(assistStructure, null)
} }
@@ -127,11 +136,13 @@ object AutofillHelper {
return this return this
} }
private fun buildDatasetForEntry(context: Context, private fun buildDatasetForEntry(
database: ContextualDatabase, context: Context,
entryInfo: EntryInfo, database: ContextualDatabase,
struct: StructureParser.Result, entryInfo: EntryInfo,
inlinePresentation: InlinePresentation?): Dataset { struct: StructureParser.Result,
inlinePresentation: InlinePresentation?
): Dataset {
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon) val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -291,11 +302,13 @@ object AutofillHelper {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
private fun buildInlinePresentationForEntry(context: Context, private fun buildInlinePresentationForEntry(
database: ContextualDatabase, context: Context,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest, database: ContextualDatabase,
positionItem: Int, compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
entryInfo: EntryInfo): InlinePresentation? { positionItem: Int,
entryInfo: EntryInfo
): InlinePresentation? {
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
@@ -341,9 +354,11 @@ object AutofillHelper {
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun buildInlinePresentationForManualSelection(context: Context, private fun buildInlinePresentationForManualSelection(
inlinePresentationSpec: InlinePresentationSpec, context: Context,
pendingIntent: PendingIntent): InlinePresentation? { inlinePresentationSpec: InlinePresentationSpec,
pendingIntent: PendingIntent
): InlinePresentation? {
// Make sure that the IME spec claims support for v1 UI template. // Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
@@ -360,11 +375,14 @@ object AutofillHelper {
}.build().slice, inlinePresentationSpec, false) }.build().slice, inlinePresentationSpec, false)
} }
fun buildResponse(context: Context, fun buildResponse(
database: ContextualDatabase, context: Context,
entriesInfo: List<EntryInfo>, database: ContextualDatabase,
parseResult: StructureParser.Result, entriesInfo: List<EntryInfo>,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? { parseResult: StructureParser.Result,
concreteWebDomain: String?,
autofillComponent: AutofillComponent
): FillResponse? {
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
// Add Header // Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -385,7 +403,8 @@ object AutofillHelper {
// Add inline suggestion for new IME and dataset // Add inline suggestion for new IME and dataset
var numberInlineSuggestions = 0 var numberInlineSuggestions = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size) numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) { if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
@@ -401,21 +420,27 @@ object AutofillHelper {
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& numberInlineSuggestions > 0 && numberInlineSuggestions > 0
&& compatInlineSuggestionsRequest != null) { && autofillComponent.compatInlineSuggestionsRequest != null) {
inlinePresentation = buildInlinePresentationForEntry( inlinePresentation = buildInlinePresentationForEntry(
context, context,
database, database,
compatInlineSuggestionsRequest, autofillComponent.compatInlineSuggestionsRequest,
numberInlineSuggestions--, numberInlineSuggestions--,
entry entry
) )
} }
// Create dataset for each entry // Create dataset for each entry
responseBuilder.addDataset( responseBuilder.addDataset(
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation) buildDatasetForEntry(
context = context,
database = database,
entryInfo = entry,
struct = parseResult,
inlinePresentation = inlinePresentation
)
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to add dataset") Log.e(TAG, "Unable to add dataset", e)
} }
} }
@@ -423,25 +448,32 @@ object AutofillHelper {
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
val searchInfo = SearchInfo().apply { val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId applicationId = parseResult.applicationId
webDomain = parseResult.webDomain webDomain = concreteWebDomain
webScheme = parseResult.webScheme webScheme = parseResult.webScheme
manualSelection = true manualSelection = true
} }
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry) val manualSelectionView = RemoteViews(
AutofillLauncherActivity.getPendingIntentForSelection(context, context.packageName,
searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent -> R.layout.item_autofill_select_entry
)
AutofillLauncherActivity.getPendingIntentForSelection(
context,
searchInfo,
autofillComponent
)?.let { pendingIntent ->
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> autofillComponent.compatInlineSuggestionsRequest
val inlinePresentationSpec = ?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
inlineSuggestionsRequest.inlinePresentationSpecs[0] val inlinePresentationSpec =
inlinePresentation = buildInlinePresentationForManualSelection( inlineSuggestionsRequest.inlinePresentationSpecs[0]
context, inlinePresentation = buildInlinePresentationForManualSelection(
inlinePresentationSpec, context,
pendingIntent inlinePresentationSpec,
) pendingIntent
} )
}
} }
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -486,61 +518,34 @@ object AutofillHelper {
} }
/** /**
* Build the Autofill response for one entry * Build the Autofill response
*/ */
fun buildResponseAndSetResult(activity: Activity, fun buildResponse(
database: ContextualDatabase, context: Context,
entryInfo: EntryInfo) { autofillComponent: AutofillComponent,
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) }) database: ContextualDatabase,
} entriesInfo: List<EntryInfo>,
onIntentCreated: suspend (Intent) -> Unit
/** ) {
* Build the Autofill response for many entry
*/
fun buildResponseAndSetResult(activity: Activity,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>) {
if (entriesInfo.isEmpty()) { if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED) throw IOException("No entries found")
} else { } else {
var setResultOk = false StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure -> getConcreteWebDomain(context, result.webDomain) { concreteWebDomain ->
StructureParser(structure).parse()?.let { result ->
// New Response // New Response
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { onIntentCreated(Intent().putExtra(
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>( AutofillManager.EXTRA_AUTHENTICATION_RESULT,
EXTRA_INLINE_SUGGESTIONS_REQUEST buildResponse(
context = context,
database = database,
entriesInfo = entriesInfo,
parseResult = result,
concreteWebDomain = concreteWebDomain,
autofillComponent = autofillComponent
) )
if (compatInlineSuggestionsRequest != null) { ))
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
}
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
} else {
buildResponse(activity, database, entriesInfo, result, null)
}
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Success Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
response)
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
} }
} } ?: throw IOException("Unable to parse the structure")
if (!setResultOk) {
Log.w(activity.javaClass.name, "Failed Autofill auth.")
activity.setResult(Activity.RESULT_CANCELED)
}
}
}
fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
autofillComponent.compatInlineSuggestionsRequest?.let {
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
} }
} }

View File

@@ -92,10 +92,11 @@ class KeeAutofillService : AutofillService() {
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this) autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
} }
override fun onFillRequest(request: FillRequest, override fun onFillRequest(
cancellationSignal: CancellationSignal, request: FillRequest,
callback: FillCallback) { cancellationSignal: CancellationSignal,
callback: FillCallback
) {
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") } cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) { if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
@@ -115,13 +116,12 @@ class KeeAutofillService : AutofillService() {
webDomain = parseResult.webDomain, webDomain = parseResult.webDomain,
webDomainBlocklist = webDomainBlocklist) webDomainBlocklist = webDomainBlocklist)
) { ) {
val searchInfo = SearchInfo().apply { getConcreteWebDomain(this, parseResult.webDomain) { webDomainWithoutSubDomain ->
applicationId = parseResult.applicationId val searchInfo = SearchInfo().apply {
webDomain = parseResult.webDomain applicationId = parseResult.applicationId
webScheme = parseResult.webScheme webDomain = webDomainWithoutSubDomain
} webScheme = parseResult.webScheme
searchInfo.getConcreteWebDomain(this) { webDomainWithoutSubDomain -> }
searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) { && autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request) CompatInlineSuggestionsRequest(request)
@@ -129,20 +129,26 @@ class KeeAutofillService : AutofillService() {
null null
} }
launchSelection(mDatabase, launchSelection(mDatabase,
searchInfo, searchInfo,
parseResult, parseResult,
inlineSuggestionsRequest, AutofillComponent(
callback) latestStructure,
inlineSuggestionsRequest
),
callback
)
} }
} }
} }
} }
private fun launchSelection(database: ContextualDatabase?, private fun launchSelection(
searchInfo: SearchInfo, database: ContextualDatabase?,
parseResult: StructureParser.Result, searchInfo: SearchInfo,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?, parseResult: StructureParser.Result,
callback: FillCallback) { autofillComponent: AutofillComponent,
callback: FillCallback
) {
SearchHelper.checkAutoSearchInfo( SearchHelper.checkAutoSearchInfo(
context = this, context = this,
database = database, database = database,
@@ -150,37 +156,46 @@ class KeeAutofillService : AutofillService() {
onItemsFound = { openedDatabase, items -> onItemsFound = { openedDatabase, items ->
callback.onSuccess( callback.onSuccess(
AutofillHelper.buildResponse( AutofillHelper.buildResponse(
this, openedDatabase, context = this,
items, parseResult, inlineSuggestionsRequest database = openedDatabase,
entriesInfo = items,
parseResult = parseResult,
concreteWebDomain = searchInfo.webDomain,
autofillComponent = autofillComponent
) )
) )
}, },
onItemNotFound = { openedDatabase -> onItemNotFound = { openedDatabase ->
// Show UI if no search result // Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase, showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, inlineSuggestionsRequest, callback) searchInfo, autofillComponent, callback)
}, },
onDatabaseClosed = { onDatabaseClosed = {
// Show UI if database not open // Show UI if database not open
showUIForEntrySelection(parseResult, null, showUIForEntrySelection(parseResult, null,
searchInfo, inlineSuggestionsRequest, callback) searchInfo, autofillComponent, callback)
} }
) )
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(
database: ContextualDatabase?, parseResult: StructureParser.Result,
searchInfo: SearchInfo, database: ContextualDatabase?,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?, searchInfo: SearchInfo,
callback: FillCallback) { autofillComponent: AutofillComponent,
callback: FillCallback
) {
var success = false var success = false
parseResult.allAutofillIds().let { autofillIds -> parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) { if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used // If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response. // to generate Response.
AutofillLauncherActivity.getPendingIntentForSelection(this, AutofillLauncherActivity.getPendingIntentForSelection(
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender -> this,
searchInfo,
autofillComponent
)?.intentSender?.let { intentSender ->
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) { val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) { if (!parseResult.webDomain.isNullOrEmpty()) {
@@ -271,7 +286,8 @@ class KeeAutofillService : AutofillService() {
&& autofillInlineSuggestionsEnabled && autofillInlineSuggestionsEnabled
) { ) {
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = val inlinePresentationSpecs =
inlineSuggestionsRequest.inlinePresentationSpecs inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0 if (inlineSuggestionsRequest.maxSuggestionCount > 0
@@ -387,33 +403,40 @@ class KeeAutofillService : AutofillService() {
} }
// Show UI to save data // Show UI to save data
val registerInfo = RegisterInfo( getConcreteWebDomain(applicationContext, parseResult.webDomain) { concreteWebDomain ->
searchInfo = SearchInfo().apply { val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId applicationId = parseResult.applicationId
webDomain = parseResult.webDomain webDomain = concreteWebDomain
webScheme = parseResult.webScheme webScheme = parseResult.webScheme
},
username = parseResult.usernameValue?.textValue?.toString(),
password = parseResult.passwordValue?.textValue?.toString(),
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
CreditCard(
parseResult.creditCardHolder,
cardNumber,
expiration,
parseResult.cardVerificationValue
)
} }
) val registerInfo = RegisterInfo(
searchInfo = searchInfo,
username = parseResult.usernameValue?.textValue?.toString(),
password = parseResult.passwordValue?.textValue?.toString(),
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
CreditCard(
parseResult.creditCardHolder,
cardNumber,
expiration,
parseResult.cardVerificationValue
)
}
)
// TODO Callback in each activity #765 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // TODO Test pending intent
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this, AutofillLauncherActivity.getPendingIntentForRegistration(
// registerInfo)) this,
//} else { registerInfo
AutofillLauncherActivity.launchForRegistration(this, registerInfo) )?.intentSender?.let { intentSender ->
success = true callback.onSuccess(intentSender)
callback.onSuccess() } ?: callback.onFailure("Unable to launch registration")
//} } else {
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
success = true
callback.onSuccess()
}
}
} }
} }
} }

View File

@@ -43,6 +43,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.FieldsAdapter import com.kunzisoft.keepass.adapters.FieldsAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider import com.kunzisoft.keepass.database.DatabaseTaskProvider
@@ -484,7 +485,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
// Populate Magikeyboard with entry // Populate Magikeyboard with entry
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast) addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode // Consume the selection mode
EntrySelectionHelper.removeModesFromIntent(activity.intent) activity.intent.removeModes()
activity.moveTaskToBack(true) activity.moveTaskToBack(true)
} }
} }

View File

@@ -24,7 +24,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.ParcelUuid
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Log import android.util.Log
@@ -44,6 +43,7 @@ import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.Signature import com.kunzisoft.encrypt.Signature
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
@@ -60,7 +60,6 @@ import com.kunzisoft.keepass.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
@@ -88,10 +87,7 @@ object PasskeyHelper {
private const val HMAC_TYPE = "HmacSHA256" private const val HMAC_TYPE = "HmacSHA256"
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin" private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp" private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode" private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
@@ -110,38 +106,6 @@ object PasskeyHelper {
private val internalSecureRandom: SecureRandom = SecureRandom() private val internalSecureRandom: SecureRandom = SecureRandom()
/**
* Build the Passkey response for one entry
*/
fun Activity.buildPasskeyResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
try {
entryInfo.passkey?.let { passkey ->
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.addPasskey(passkey)
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
mReplyIntent.addNodeId(entryInfo.id)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
throw IOException("No passkey found")
}
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the passkey as result", e)
Toast.makeText(
this,
getString(R.string.error_passkey_result),
Toast.LENGTH_SHORT
).show()
setResult(Activity.RESULT_CANCELED)
}
}
/** /**
* Add an authentication code generated by an entry to the intent * Add an authentication code generated by an entry to the intent
*/ */
@@ -181,22 +145,6 @@ object PasskeyHelper {
return this.removeExtra(EXTRA_PASSKEY) return this.removeExtra(EXTRA_PASSKEY)
} }
/**
* Add the search info to the intent
*/
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
searchInfo?.let {
putExtra(EXTRA_SEARCH_INFO, searchInfo)
}
}
/**
* Retrieve the search info from the intent
*/
fun Intent.retrieveSearchInfo(): SearchInfo? {
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
}
/** /**
* Add the app origin to the intent * Add the app origin to the intent
*/ */
@@ -221,21 +169,37 @@ object PasskeyHelper {
} }
/** /**
* Add the node id to the intent, useful for auto passkey selection * Build the Passkey response for one entry
*/ */
fun Intent.addNodeId(nodeId: UUID?) { fun Activity.buildPasskeyResponseAndSetResult(
nodeId?.let { entryInfo: EntryInfo,
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId)) extras: Bundle? = null
) {
try {
entryInfo.passkey?.let { passkey ->
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.addPasskey(passkey)
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
mReplyIntent.addNodeId(entryInfo.id)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
throw IOException("No passkey found")
}
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the passkey as result", e)
Toast.makeText(
this,
getString(R.string.error_passkey_result),
Toast.LENGTH_SHORT
).show()
setResult(Activity.RESULT_CANCELED)
} }
} }
/**
* Retrieve the node id from the intent
*/
fun Intent.retrieveNodeId(): UUID? {
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
}
/** /**
* Check the timestamp and authentication code transmitted via PendingIntent * Check the timestamp and authentication code transmitted via PendingIntent
*/ */

View File

@@ -0,0 +1,301 @@
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.os.Build
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.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
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.helper.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
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
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
private var mAutofillComponent: AutofillComponent? = null
private var mSelectionResult: ActivityResult? = null
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
override fun onResult() {
super.onResult()
mAutofillComponent = null
mSelectionResult = null
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
if (database != null) {
mSelectionResult?.let { selectionResult ->
manageSelectionResult(database, selectionResult)
}
}
}
override suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
// Retrieve selection mode
when (intent.retrieveSpecialMode()) {
SpecialMode.SELECTION -> {
val searchInfo = intent.retrieveSearchInfo()
if (searchInfo == null)
throw IOException("Search info is null")
mAutofillComponent = intent.retrieveAutofillComponent()
// Build search param
launchSelection(database, mAutofillComponent, searchInfo)
}
SpecialMode.REGISTRATION -> {
// To register info
val registerInfo = intent.retrieveRegisterInfo()
if (registerInfo == null)
throw IOException("Register info is null")
launchRegistration(database, registerInfo)
}
else -> {
// Not an autofill call
cancelResult()
}
}
}
private suspend fun launchSelection(
database: ContextualDatabase?,
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo
) {
withContext(Dispatchers.IO) {
if (autofillComponent == null) {
throw IOException("Autofill component is null")
}
if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = getApplication()
)
) {
// If database is open
SearchHelper.checkAutoSearchInfo(
context = getApplication(),
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Items found
if (autofillComponent.compatInlineSuggestionsRequest != null) {
mUiState.value = UIState.ShowAutofillSuggestionMessage
}
AutofillHelper.buildResponse(
context = getApplication(),
autofillComponent = autofillComponent,
database = openedDatabase,
entriesInfo = items
) { intent ->
setResult(intent)
}
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.AUTOFILL
)
},
onDatabaseClosed = {
// If database not open
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo,
typeMode = TypeMode.AUTOFILL
)
}
)
} else {
mUiState.value = UIState.ShowBlockRestartMessage
}
}
}
fun manageSelectionResult(activityResult: ActivityResult) {
mSelectionResult = activityResult
// Waiting for the database if needed
mDatabase?.let { database ->
manageSelectionResult(database, activityResult)
}
}
private fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult
) {
mSelectionResult = null
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for autofill", e)
showError(e)
}) {
when (activityResult.resultCode) {
RESULT_OK -> {
withContext(Dispatchers.IO) {
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 autofillComponent = mAutofillComponent
if (autofillComponent == null)
throw IOException("Autofill component is null")
val entries = nodesIds.mapNotNull { nodeId ->
database
.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
}
AutofillHelper.buildResponse(
context = getApplication(),
autofillComponent = autofillComponent,
database = database,
entriesInfo = entries
) { intent ->
withContext(Dispatchers.Main) {
setResult(intent)
}
}
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()
}
}
}
}
}
// -------------
// Registration
// -------------
private fun launchRegistration(
database: ContextualDatabase?,
registerInfo: RegisterInfo
) {
val searchInfo = registerInfo.searchInfo
if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = getApplication()
)) {
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(
context = getApplication(),
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, _ ->
if (!readOnly) {
// Show the database UI to select the entry
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
mUiState.value = UIState.ShowReadOnlyMessage
}
},
onItemNotFound = { openedDatabase ->
if (!readOnly) {
// Show the database UI to select the entry
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
mUiState.value = UIState.ShowReadOnlyMessage
}
},
onDatabaseClosed = {
// If database not open
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
}
)
} else {
mUiState.value = UIState.ShowBlockRestartMessage
}
}
fun manageRegistrationResult(
activityResult: ActivityResult
) {
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create registration response for autofill", e)
showError(e)
}) {
val responseIntent = Intent()
when (activityResult.resultCode) {
RESULT_OK -> {
withContext(Dispatchers.IO) {
Log.d(TAG, "Autofill registration result")
// TODO Result
withContext(Dispatchers.Main) {
setResult(responseIntent)
}
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()
}
}
}
}
}
sealed class UIState {
object Loading: UIState()
object ShowBlockRestartMessage: UIState()
object ShowReadOnlyMessage: UIState()
object ShowAutofillSuggestionMessage: UIState()
}
companion object {
private val TAG = AutofillLauncherViewModel::class.java.name
}
}

View File

@@ -0,0 +1,119 @@
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.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
abstract class CredentialLauncherViewModel(application: Application): AndroidViewModel(application) {
protected var mDatabase: ContextualDatabase? = null
protected var mLockDatabase: Boolean = true
protected var isResultLauncherRegistered: Boolean = false
protected val mCredentialUiState = MutableStateFlow<UIState>(UIState.Loading)
val credentialUiState: StateFlow<UIState> = mCredentialUiState
fun showError(error: Throwable) {
Log.e(TAG, "Error on credential provider launch", error)
mCredentialUiState.value = UIState.ShowError(error)
}
open fun onResult() {
isResultLauncherRegistered = false
}
fun setResult(intent: Intent) {
// Remove the launcher register
onResult()
mCredentialUiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_OK,
data = intent
)
}
fun cancelResult() {
onResult()
mCredentialUiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_CANCELED
)
}
open fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
}
fun launchActionIfNeeded(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
onDatabaseRetrieved(database)
if (isResultLauncherRegistered.not()) {
isResultLauncherRegistered = true
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
showError(e)
}) {
launchAction(intent, specialMode, database)
}
}
}
/**
* Launch the main action
*/
protected abstract suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
)
sealed class UIState {
object Loading : UIState()
data class LaunchGroupActivityForSelection(
val database: ContextualDatabase,
val searchInfo: SearchInfo?,
val typeMode: TypeMode
): UIState()
data class LaunchGroupActivityForRegistration(
val database: ContextualDatabase,
val registerInfo: RegisterInfo?,
val typeMode: TypeMode
): UIState()
data class LaunchFileDatabaseSelectActivityForSelection(
val searchInfo: SearchInfo?,
val typeMode: TypeMode
): UIState()
data class LaunchFileDatabaseSelectActivityForRegistration(
val registerInfo: RegisterInfo?,
val typeMode: TypeMode
): UIState()
data class SetActivityResult(
val lockDatabase: Boolean,
val resultCode: Int,
val data: Intent? = null
): UIState()
data class ShowError(
val error: Throwable
): UIState()
}
companion object {
private val TAG = CredentialLauncherViewModel::class.java.name
}
}

View File

@@ -13,6 +13,8 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PendingIntentHandler
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
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.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
@@ -25,11 +27,9 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVe
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
@@ -261,14 +261,17 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
"launch manual selection in opened database" "launch manual selection in opened database"
) )
_uiState.value = UIState.LaunchGroupActivityForSelection( _uiState.value = UIState.LaunchGroupActivityForSelection(
database = openedDatabase database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
) )
}, },
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database") Log.d(TAG, "Manual passkey selection in closed database")
_uiState.value = _uiState.value =
UIState.LaunchFileDatabaseSelectActivityForSelection( UIState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
) )
} }
) )
@@ -550,7 +553,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val nodeId: UUID val nodeId: UUID
): UIState() ): UIState()
data class LaunchGroupActivityForSelection( data class LaunchGroupActivityForSelection(
val database: ContextualDatabase val database: ContextualDatabase,
val searchInfo: SearchInfo?,
val typeMode: TypeMode
): UIState() ): UIState()
data class LaunchGroupActivityForRegistration( data class LaunchGroupActivityForRegistration(
val database: ContextualDatabase, val database: ContextualDatabase,
@@ -558,7 +563,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val typeMode: TypeMode val typeMode: TypeMode
): UIState() ): UIState()
data class LaunchFileDatabaseSelectActivityForSelection( data class LaunchFileDatabaseSelectActivityForSelection(
val searchInfo: SearchInfo val searchInfo: SearchInfo,
val typeMode: TypeMode
): UIState() ): UIState()
data class LaunchFileDatabaseSelectActivityForRegistration( data class LaunchFileDatabaseSelectActivityForRegistration(
val registerInfo: RegisterInfo, val registerInfo: RegisterInfo,

View File

@@ -54,6 +54,7 @@ object SearchHelper {
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit, onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
onDatabaseClosed: () -> Unit onDatabaseClosed: () -> Unit
) { ) {
// TODO suspend
if (database == null || !database.loaded) { if (database == null || !database.loaded) {
onDatabaseClosed.invoke() onDatabaseClosed.invoke()
} else if (TimeoutHelper.checkTime(context)) { } else if (TimeoutHelper.checkTime(context)) {

View File

@@ -23,6 +23,10 @@ import mozilla.components.lib.publicsuffixlist.PublicSuffixList
object AppUtil { object AppUtil {
fun randomRequestCode(): Int {
return (Math.random() * Integer.MAX_VALUE).toInt()
}
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean { fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
try { try {
this.applicationContext.packageManager.getPackageInfoCompat( this.applicationContext.packageManager.getPackageInfoCompat(
@@ -83,9 +87,10 @@ object AppUtil {
/** /**
* Get the concrete web domain AKA without sub domain if needed * Get the concrete web domain AKA without sub domain if needed
*/ */
fun SearchInfo.getConcreteWebDomain( fun getConcreteWebDomain(
context: Context, context: Context,
concreteWebDomain: (String?) -> Unit webDomain: String?,
concreteWebDomain: suspend (String?) -> Unit
) { ) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val domain = webDomain val domain = webDomain

View File

@@ -39,7 +39,7 @@ inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(key: Str
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
} }
inline fun <reified E : Parcelable> Intent.putParcelableList(key: String?, list: MutableList<E>) { inline fun <reified E : Parcelable> Intent.putParcelableList(key: String?, list: List<E>) {
putExtra(key, list.toTypedArray()) putExtra(key, list.toTypedArray())
} }