diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index ab2d1580c..9d2d140ee 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -56,9 +56,10 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter 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.autofill.AutofillComponent -import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult import com.kunzisoft.keepass.database.ContextualDatabase @@ -203,8 +204,8 @@ class EntryEditActivity : DatabaseLockActivity(), mDatabase, entryId, parentId, - EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent) - ?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)?.toRegisterInfo() + intent.retrieveRegisterInfo() + ?: intent.retrieveSearchInfo()?.toRegisterInfo() ) // To retrieve attachment @@ -378,7 +379,7 @@ class EntryEditActivity : DatabaseLockActivity(), intent = intent, defaultAction = {}, searchAction = {}, - selectionAction = { intentSender, typeMode, searchInfo, autofillComponent -> + selectionAction = { intentSender, typeMode, searchInfo -> when(typeMode) { TypeMode.DEFAULT -> {} TypeMode.MAGIKEYBOARD -> @@ -396,7 +397,7 @@ class EntryEditActivity : DatabaseLockActivity(), TypeMode.PASSKEY -> entryValidatedForPasskeyRegistration(database, entrySave.newEntry) TypeMode.AUTOFILL -> - entryValidatedForAutofillRegistration(entrySave.newEntry) + entryValidatedForAutofillRegistration(database, entrySave.newEntry) } } ) @@ -444,7 +445,7 @@ class EntryEditActivity : DatabaseLockActivity(), searchAction = { // Nothing when search retrieved }, - selectionAction = { intentSender, typeMode, searchInfo, autofillComponent -> + selectionAction = { intentSender, typeMode, searchInfo -> when(typeMode) { TypeMode.DEFAULT -> {} TypeMode.MAGIKEYBOARD -> @@ -463,7 +464,7 @@ class EntryEditActivity : DatabaseLockActivity(), TypeMode.PASSKEY -> entryValidatedForPasskeyRegistration(database, entry) TypeMode.AUTOFILL -> - entryValidatedForAutofillRegistration(entry) + entryValidatedForAutofillRegistration(database, entry) } } ) @@ -496,9 +497,10 @@ class EntryEditActivity : DatabaseLockActivity(), private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) { // Build Autofill response with the entry selected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity, - database, - entry.getEntryInfo(database)) + this.buildSpecialModeResponseAndSetResult( + entryInfo = entry.getEntryInfo(database), + extras = buildEntryResult(entry) + ) } onValidateSpecialMode() } @@ -506,20 +508,21 @@ class EntryEditActivity : DatabaseLockActivity(), private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { this.buildPasskeyResponseAndSetResult( - entryInfo = entry.getEntryInfo(database) + entryInfo = entry.getEntryInfo(database), + extras = buildEntryResult(entry) ) } onValidateSpecialMode() } - private fun entryValidatedForAutofillRegistration(entry: Entry) { - //if (isIntentSender()) { - // TODO Autofill Callback #765 - //} - onValidateSpecialMode() - if (!isIntentSender()) { - finishForEntryResult(entry) + private fun entryValidatedForAutofillRegistration(database: ContextualDatabase, entry: Entry) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + this.buildSpecialModeResponseAndSetResult( + entryInfo = entry.getEntryInfo(database), + extras = buildEntryResult(entry) + ) } + onValidateSpecialMode() } private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) { @@ -856,7 +859,6 @@ class EntryEditActivity : DatabaseLockActivity(), typeMode: TypeMode, groupId: NodeId<*>, searchInfo: SearchInfo? = null, - autofillComponent: AutofillComponent? = null, activityResultLauncher: ActivityResultLauncher? = null, ) { if (database.loaded && !database.isReadOnly) { @@ -868,7 +870,6 @@ class EntryEditActivity : DatabaseLockActivity(), intent = intent, typeMode = typeMode, searchInfo = searchInfo, - autofillComponent = autofillComponent, activityResultLauncher = activityResultLauncher ) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index 26a6da6b6..88e03ac77 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -50,7 +50,6 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.TypeMode -import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation @@ -303,7 +302,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), ) onLaunchActivitySpecialMode() }, - selectionAction = { intentSenderMode, typeMode, searchInfo, autofillComponent -> + selectionAction = { intentSenderMode, typeMode, searchInfo -> MainCredentialActivity.launchForSelection( activity = this, activityResultLauncher = if (intentSenderMode) @@ -312,8 +311,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), keyFile = keyFile, hardwareKey = hardwareKey, typeMode = typeMode, - searchInfo = searchInfo, - autofillComponent = autofillComponent, + searchInfo = searchInfo ) onLaunchActivitySpecialMode() }, @@ -496,18 +494,16 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), */ fun launchForSelection( - activity: Activity, + context: Context, typeMode: TypeMode, searchInfo: SearchInfo? = null, - autofillComponent: AutofillComponent? = null, activityResultLauncher: ActivityResultLauncher? = null, ) { EntrySelectionHelper.startActivityForSelectionModeResult( - context = activity, - intent = Intent(activity, FileDatabaseSelectActivity::class.java), + context = context, + intent = Intent(context, FileDatabaseSelectActivity::class.java), searchInfo = searchInfo, typeMode = typeMode, - autofillComponent = autofillComponent, activityResultLauncher = activityResultLauncher ) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index aa38522b9..54dc4bc14 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -64,10 +64,13 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.adapters.BreadcrumbAdapter 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.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.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult import com.kunzisoft.keepass.database.ContextualDatabase @@ -495,14 +498,13 @@ class GroupActivity : DatabaseLockActivity(), searchAction = { // Search not used }, - selectionAction = { intentSenderMode, typeMode, searchInfo, autofillComponent -> + selectionAction = { intentSenderMode, typeMode, searchInfo -> EntryEditActivity.launchForSelection( context = this@GroupActivity, database = database, typeMode = typeMode, groupId = currentGroup.nodeId, searchInfo = searchInfo, - autofillComponent = autofillComponent, activityResultLauncher = if (intentSenderMode) mCredentialActivityResultLauncher else null ) @@ -666,7 +668,7 @@ class GroupActivity : DatabaseLockActivity(), searchAction = { // Search not used }, - selectionAction = { intentSenderMode, typeMode, searchInfo, autofillComponent -> + selectionAction = { intentSenderMode, typeMode, searchInfo -> when (typeMode) { TypeMode.DEFAULT -> {} TypeMode.MAGIKEYBOARD -> entry?.let { @@ -700,7 +702,7 @@ class GroupActivity : DatabaseLockActivity(), */ private fun transformSearchInfoIntent(intent: Intent) { // 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) intent.removeExtra(AUTO_SEARCH_KEY) if (searchInfo != null && autoSearch) { @@ -840,7 +842,7 @@ class GroupActivity : DatabaseLockActivity(), searchAction = { // Nothing here, a search is simply performed }, - selectionAction = { intentSenderMode, typeMode, searchInfo, autofillComponent -> + selectionAction = { intentSenderMode, typeMode, searchInfo -> when (typeMode) { TypeMode.DEFAULT -> {} TypeMode.MAGIKEYBOARD -> { @@ -910,11 +912,7 @@ class GroupActivity : DatabaseLockActivity(), removeSearch() // Build response with the entry selected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.buildResponseAndSetResult( - this, - database, - entry.getEntryInfo(database) - ) + this.buildSpecialModeResponseAndSetResult(entry.getEntryInfo(database)) } onValidateSpecialMode() } @@ -1381,8 +1379,8 @@ class GroupActivity : DatabaseLockActivity(), // Else in root, lock if needed else { removeSearch() - EntrySelectionHelper.removeModesFromIntent(intent) - EntrySelectionHelper.removeInfoFromIntent(intent) + intent.removeModes() + intent.removeInfo() if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) { lockAndExit() } else { @@ -1513,10 +1511,7 @@ class GroupActivity : DatabaseLockActivity(), if (database.loaded) { checkTimeAndBuildIntent(context, null) { intent -> intent.putExtra(AUTO_SEARCH_KEY, autoSearch) - EntrySelectionHelper.addSearchInfoInIntent( - intent, - searchInfo - ) + intent.addSearchInfo(searchInfo) context.startActivity(intent) } } @@ -1528,7 +1523,6 @@ class GroupActivity : DatabaseLockActivity(), typeMode: TypeMode, searchInfo: SearchInfo? = null, autoSearch: Boolean = false, - autofillComponent: AutofillComponent? = null, activityResultLauncher: ActivityResultLauncher? = null, ) { if (database.loaded) { @@ -1539,7 +1533,6 @@ class GroupActivity : DatabaseLockActivity(), intent = intent, typeMode = typeMode, searchInfo = searchInfo, - autofillComponent = autofillComponent, activityResultLauncher = activityResultLauncher ) } @@ -1608,7 +1601,7 @@ class GroupActivity : DatabaseLockActivity(), onCancelSpecialMode() } }, - selectionAction = { intentSenderMode, typeMode, searchInfo, autofillComponent -> + selectionAction = { intentSenderMode, typeMode, searchInfo -> SearchHelper.checkAutoSearchInfo( context = activity, database = database, @@ -1644,7 +1637,9 @@ class GroupActivity : DatabaseLockActivity(), EntrySelectionHelper.performSelection( items = items, actionPopulateCredentialProvider = { entryInfo -> - activity.buildPasskeyResponseAndSetResult(entryInfo) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activity.buildPasskeyResponseAndSetResult(entryInfo) + } onValidateSpecialMode() }, actionEntrySelection = { @@ -1662,7 +1657,7 @@ class GroupActivity : DatabaseLockActivity(), } TypeMode.AUTOFILL -> { // Response is build - AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items) + activity.buildSpecialModeResponseAndSetResult(items) onValidateSpecialMode() } } @@ -1675,7 +1670,6 @@ class GroupActivity : DatabaseLockActivity(), typeMode = typeMode, searchInfo = searchInfo, autoSearch = false, - autofillComponent = autofillComponent, activityResultLauncher = if (intentSenderMode) activityResultLauncher else null ) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index 4ff2dae68..9ccb43149 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -57,7 +57,6 @@ import com.kunzisoft.keepass.biometric.deviceUnlockError import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.TypeMode -import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException @@ -815,7 +814,6 @@ class MainCredentialActivity : DatabaseModeActivity() { hardwareKey: HardwareKey?, typeMode: TypeMode, searchInfo: SearchInfo?, - autofillComponent: AutofillComponent? = null, activityResultLauncher: ActivityResultLauncher? = null, ) { buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> @@ -824,7 +822,6 @@ class MainCredentialActivity : DatabaseModeActivity() { intent = intent, typeMode = typeMode, searchInfo = searchInfo, - autofillComponent = autofillComponent, activityResultLauncher = activityResultLauncher ) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt index 8d892dab8..610c24109 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt @@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE import com.kunzisoft.keepass.R 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.credentialprovider.EntrySelectionHelper.retrieveSpecialMode +import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.SortNodeEnum @@ -248,7 +248,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener) activity?.intent?.let { - specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it) + specialMode = it.retrieveSpecialMode() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt index fc760afbf..70d0ec1c4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt @@ -34,7 +34,7 @@ import androidx.appcompat.app.AlertDialog import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment 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.database.ContextualDatabase import com.kunzisoft.keepass.database.MainCredential @@ -395,7 +395,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), // If in registration mode, don't allow read only if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) { Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show() - EntrySelectionHelper.removeModesFromIntent(intent) + intent.removeModes() finish() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt index 5a181f803..0faaf8868 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt @@ -7,9 +7,14 @@ import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.result.ActivityResultLauncher import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher 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.TypeMode import com.kunzisoft.keepass.model.RegisterInfo @@ -56,8 +61,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() { fun onLaunchActivitySpecialMode() { if (!isIntentSender()) { - EntrySelectionHelper.removeModesFromIntent(intent) - EntrySelectionHelper.removeInfoFromIntent(intent) + intent.removeModes() + intent.removeInfo() finish() } } @@ -66,8 +71,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() { if (isIntentSender()) { super.finish() } else { - EntrySelectionHelper.removeModesFromIntent(intent) - EntrySelectionHelper.removeInfoFromIntent(intent) + intent.removeModes() + intent.removeInfo() if (mSpecialMode != SpecialMode.DEFAULT) { backToTheMainAppAndFinish() } @@ -79,8 +84,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() { // To get the app caller, only for IntentSender onRegularBackPressed() } else { - EntrySelectionHelper.removeModesFromIntent(intent) - EntrySelectionHelper.removeInfoFromIntent(intent) + intent.removeModes() + intent.removeInfo() if (mSpecialMode != SpecialMode.DEFAULT) { backToTheMainAppAndFinish() } @@ -111,18 +116,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() { } }) - mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent) - mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent) + mSpecialMode = intent.retrieveSpecialMode() + mTypeMode = intent.retrieveTypeMode() } override fun onResume() { super.onResume() - mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent) - mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent) - val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent) + mSpecialMode = intent.retrieveSpecialMode() + mTypeMode = intent.retrieveTypeMode() + val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo() val searchInfo: SearchInfo? = registerInfo?.searchInfo - ?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) + ?: intent.retrieveSearchInfo() // To show the selection mode mToolbarSpecial = findViewById(R.id.special_mode_view) diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/EntrySelectionHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/EntrySelectionHelper.kt index 897b0d251..97abc4b18 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/EntrySelectionHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/EntrySelectionHelper.kt @@ -24,6 +24,8 @@ import android.content.Context import android.content.Intent import android.graphics.drawable.Icon import android.os.Build +import android.os.Bundle +import android.os.ParcelUuid import android.util.Log import android.widget.RemoteViews import androidx.activity.result.ActivityResultLauncher @@ -32,9 +34,6 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat 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.model.EntryInfo 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.getEnumExtra import com.kunzisoft.keepass.utils.getParcelableExtraCompat +import com.kunzisoft.keepass.utils.getParcelableList import com.kunzisoft.keepass.utils.putEnumExtra +import com.kunzisoft.keepass.utils.putParcelableList +import java.util.UUID object EntrySelectionHelper { @@ -51,6 +53,8 @@ object EntrySelectionHelper { 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_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 @@ -107,8 +111,8 @@ object EntrySelectionHelper { intent: Intent, searchInfo: SearchInfo ) { - addSpecialModeInIntent(intent, SpecialMode.SEARCH) - addSearchInfoInIntent(intent, searchInfo) + intent.addSpecialMode(SpecialMode.SEARCH) + intent.addSearchInfo(searchInfo) intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK context.startActivity(intent) } @@ -118,17 +122,11 @@ object EntrySelectionHelper { intent: Intent, typeMode: TypeMode, searchInfo: SearchInfo?, - autofillComponent: AutofillComponent? = null, activityResultLauncher: ActivityResultLauncher? = null, ) { - addSpecialModeInIntent(intent, SpecialMode.SELECTION) - addTypeModeInIntent(intent, typeMode) - addSearchInfoInIntent(intent, searchInfo) - autofillComponent?.let { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - intent.addAutofillComponent(context, autofillComponent) - } - } + intent.addSpecialMode(SpecialMode.SELECTION) + intent.addTypeMode(typeMode) + intent.addSearchInfo(searchInfo) if (activityResultLauncher == null) { intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK } @@ -142,77 +140,123 @@ object EntrySelectionHelper { registerInfo: RegisterInfo?, typeMode: TypeMode ) { - addSpecialModeInIntent(intent, SpecialMode.REGISTRATION) - addTypeModeInIntent(intent, typeMode) - addRegisterInfoInIntent(intent, registerInfo) + intent.addSpecialMode(SpecialMode.REGISTRATION) + intent.addTypeMode(typeMode) + intent.addRegisterInfo(registerInfo) if (activityResultLauncher == null) { intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK } 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, + 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 { - intent.putExtra(KEY_SEARCH_INFO, it) + putExtra(KEY_SEARCH_INFO, it) } + return this } - fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? { - return intent.getParcelableExtraCompat(KEY_SEARCH_INFO) + fun Intent.retrieveSearchInfo(): SearchInfo? { + return getParcelableExtraCompat(KEY_SEARCH_INFO) } - private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) { + fun Intent.addRegisterInfo(registerInfo: RegisterInfo?): Intent { registerInfo?.let { - intent.putExtra(KEY_REGISTER_INFO, it) + putExtra(KEY_REGISTER_INFO, it) } + return this } - fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? { - return intent.getParcelableExtraCompat(KEY_REGISTER_INFO) + fun Intent.retrieveRegisterInfo(): RegisterInfo? { + return getParcelableExtraCompat(KEY_REGISTER_INFO) } - fun removeInfoFromIntent(intent: Intent) { - intent.removeExtra(KEY_SEARCH_INFO) - intent.removeExtra(KEY_REGISTER_INFO) + fun Intent.removeInfo() { + removeExtra(KEY_SEARCH_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 { this.putEnumExtra(KEY_SPECIAL_MODE, specialMode) return this } - fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (AutofillHelper.retrieveAutofillComponent(intent) != null) - return SpecialMode.SELECTION - } - return intent.getEnumExtra(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT + fun Intent.retrieveSpecialMode(): SpecialMode { + return getEnumExtra(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 { this.putEnumExtra(KEY_TYPE_MODE, typeMode) return this } - fun retrieveTypeModeFromIntent(intent: Intent): TypeMode { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (AutofillHelper.retrieveAutofillComponent(intent) != null) - return TypeMode.AUTOFILL - } - return intent.getEnumExtra(KEY_TYPE_MODE) ?: TypeMode.DEFAULT + fun Intent.retrieveTypeMode(): TypeMode { + return getEnumExtra(KEY_TYPE_MODE) ?: TypeMode.DEFAULT } - fun removeModesFromIntent(intent: Intent) { - intent.removeExtra(KEY_SPECIAL_MODE) - intent.removeExtra(KEY_TYPE_MODE) + fun Intent.removeModes() { + removeExtra(KEY_SPECIAL_MODE) + removeExtra(KEY_TYPE_MODE) + } + + fun Intent.addNodesIds(nodesIds: List): Intent { + this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) }) + return this + } + + fun Intent.retrieveNodesIds(): List? { + return getParcelableList(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(EXTRA_NODE_ID)?.uuid } /** @@ -221,9 +265,8 @@ object EntrySelectionHelper { fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean { return (specialMode == SpecialMode.SELECTION && (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY)) - // TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P || (specialMode == SpecialMode.REGISTRATION - && typeMode == TypeMode.PASSKEY) + && (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY)) } fun doSpecialAction( @@ -233,8 +276,7 @@ object EntrySelectionHelper { selectionAction: ( intentSenderMode: Boolean, typeMode: TypeMode, - searchInfo: SearchInfo?, - autofillComponent: AutofillComponent? + searchInfo: SearchInfo? ) -> Unit, registrationAction: ( intentSenderMode: Boolean, @@ -242,16 +284,16 @@ object EntrySelectionHelper { registerInfo: RegisterInfo? ) -> Unit ) { - when (val specialMode = retrieveSpecialModeFromIntent(intent)) { + when (val specialMode = intent.retrieveSpecialMode()) { SpecialMode.DEFAULT -> { - removeModesFromIntent(intent) - removeInfoFromIntent(intent) + intent.removeModes() + intent.removeInfo() defaultAction.invoke() } SpecialMode.SEARCH -> { - val searchInfo = retrieveSearchInfoFromIntent(intent) - removeModesFromIntent(intent) - removeInfoFromIntent(intent) + val searchInfo = intent.retrieveSearchInfo() + intent.removeModes() + intent.removeInfo() if (searchInfo != null) searchAction.invoke(searchInfo) else { @@ -259,66 +301,55 @@ object EntrySelectionHelper { } } SpecialMode.SELECTION -> { - val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent) - var autofillComponentInit = false - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent -> - selectionAction.invoke( - isIntentSenderMode(specialMode, TypeMode.AUTOFILL), - TypeMode.AUTOFILL, - searchInfo, - autofillComponent - ) - autofillComponentInit = true - } - } - if (!autofillComponentInit) { - if (intent.getEnumExtra(KEY_SPECIAL_MODE) != null) { - when (val typeMode = retrieveTypeModeFromIntent(intent)) { - TypeMode.DEFAULT -> { - removeModesFromIntent(intent) - if (searchInfo != null) - searchAction.invoke(searchInfo) - else - defaultAction.invoke() - } - TypeMode.MAGIKEYBOARD -> selectionAction.invoke( - isIntentSenderMode(specialMode, typeMode), - typeMode, - searchInfo, - null - ) - TypeMode.PASSKEY -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - selectionAction.invoke( - isIntentSenderMode(specialMode, typeMode), - typeMode, - searchInfo, - null - ) - } else - defaultAction.invoke() - else -> { - // In this case, error - removeModesFromIntent(intent) - removeInfoFromIntent(intent) - } + val searchInfo: SearchInfo? = intent.retrieveSearchInfo() + if (intent.getEnumExtra(KEY_SPECIAL_MODE) != null) { + when (val typeMode = intent.retrieveTypeMode()) { + TypeMode.DEFAULT -> { + intent.removeModes() + if (searchInfo != null) + searchAction.invoke(searchInfo) + else + defaultAction.invoke() + } + TypeMode.MAGIKEYBOARD -> selectionAction.invoke( + isIntentSenderMode(specialMode, typeMode), + typeMode, + searchInfo + ) + TypeMode.PASSKEY -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + selectionAction.invoke( + isIntentSenderMode(specialMode, typeMode), + typeMode, + searchInfo + ) + } else + defaultAction.invoke() + TypeMode.AUTOFILL -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + selectionAction.invoke( + isIntentSenderMode(specialMode, typeMode), + typeMode, + searchInfo + ) + } else + defaultAction.invoke() } - } else { - if (searchInfo != null) - searchAction.invoke(searchInfo) - else - defaultAction.invoke() } + } else { + if (searchInfo != null) + searchAction.invoke(searchInfo) + else + defaultAction.invoke() } } SpecialMode.REGISTRATION -> { - val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent) - val typeMode = retrieveTypeModeFromIntent(intent) + val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo() + val typeMode = intent.retrieveTypeMode() val intentSenderMode = isIntentSenderMode(specialMode, typeMode) if (!intentSenderMode) { - removeModesFromIntent(intent) - removeInfoFromIntent(intent) + intent.removeModes() + intent.removeInfo() } if (registerInfo != null) registrationAction.invoke( diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt index 4cdcac6ee..a4859f526 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt @@ -27,34 +27,46 @@ import android.os.Bundle import android.util.Log import android.widget.Toast import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.annotation.RequiresApi +import androidx.lifecycle.lifecycleScope import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity -import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper -import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher +import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo +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.TypeMode import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent -import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper -import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest -import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService +import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent +import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel +import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException -import com.kunzisoft.keepass.database.helper.SearchHelper import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.utils.AppUtil.getConcreteWebDomain -import com.kunzisoft.keepass.utils.getParcelableCompat -import com.kunzisoft.keepass.utils.getParcelableExtraCompat +import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode import com.kunzisoft.keepass.view.toastError +import kotlinx.coroutines.launch @RequiresApi(api = Build.VERSION_CODES.O) class AutofillLauncherActivity : DatabaseModeActivity() { - override var mCredentialActivityResultLauncher: ActivityResultLauncher? = - this.buildActivityResultLauncher(typeMode = TypeMode.AUTOFILL, lockDatabase = true) + private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels() + + private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher? = + this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + autofillLauncherViewModel.manageSelectionResult(it) + } + + private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher? = + this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + autofillLauncherViewModel.manageRegistrationResult(it) + } override fun applyCustomStyle(): Boolean { return false @@ -64,176 +76,103 @@ class AutofillLauncherActivity : DatabaseModeActivity() { return true } - override fun onDatabaseRetrieved(database: ContextualDatabase?) { - super.onDatabaseRetrieved(database) - - // Retrieve selection mode - EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode -> - when (specialMode) { - SpecialMode.SELECTION -> { - intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle -> - // To pass extra inline request - var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - compatInlineSuggestionsRequest = bundle.getParcelableCompat( - KEY_INLINE_SUGGESTION - ) - } - // Build search param - bundle.getParcelableCompat(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) - } - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycleScope.launch { + // Retrieve the UI + autofillLauncherViewModel.credentialUiState.collect { uiState -> + when (uiState) { + is CredentialLauncherViewModel.UIState.Loading -> {} + is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> { + GroupActivity.launchForSelection( + context = this@AutofillLauncherActivity, + database = uiState.database, + searchInfo = uiState.searchInfo, + typeMode = uiState.typeMode, + activityResultLauncher = mAutofillSelectionActivityResultLauncher, + ) } - // Remove bundle - intent.removeExtra(KEY_SELECTION_BUNDLE) - } - SpecialMode.REGISTRATION -> { - // To register info - val registerInfo = intent.getParcelableExtraCompat( - KEY_REGISTER_INFO - ) - val searchInfo = SearchInfo(registerInfo?.searchInfo) - searchInfo.getConcreteWebDomain(this) { concreteWebDomain -> - searchInfo.webDomain = concreteWebDomain - launchRegistration(database, searchInfo, registerInfo) + is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> { + GroupActivity.launchForRegistration( + context = this@AutofillLauncherActivity, + database = uiState.database, + registerInfo = uiState.registerInfo, + typeMode = uiState.typeMode, + activityResultLauncher = mAutofillRegistrationActivityResultLauncher + ) + } + is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> { + FileDatabaseSelectActivity.launchForSelection( + 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) - finish() + } + } + lifecycleScope.launch { + // Initialize the parameters + autofillLauncherViewModel.uiState.collect { uiState -> + when (uiState) { + AutofillLauncherViewModel.UIState.Loading -> {} + is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> { + showBlockRestartMessage() + autofillLauncherViewModel.cancelResult() + } + is AutofillLauncherViewModel.UIState.ShowReadOnlyMessage -> { + showReadOnlySaveMessage() + autofillLauncherViewModel.cancelResult() + } + is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> { + showAutofillSuggestionMessage() + } } } } } - private fun launchSelection(database: ContextualDatabase?, - autofillComponent: AutofillComponent?, - searchInfo: SearchInfo) { - 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() + override fun onDatabaseRetrieved(database: ContextualDatabase?) { + super.onDatabaseRetrieved(database) + autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database) } private fun showBlockRestartMessage() { // 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() { @@ -244,27 +183,21 @@ class AutofillLauncherActivity : DatabaseModeActivity() { private val TAG = AutofillLauncherActivity::class.java.name - private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE" - private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO" - private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION" - - private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" - - fun getPendingIntentForSelection(context: Context, - searchInfo: SearchInfo? = null, - compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? { + fun getPendingIntentForSelection( + context: Context, + searchInfo: SearchInfo? = null, + autofillComponent: AutofillComponent + ): PendingIntent? { try { return PendingIntent.getActivity( - context, 0, + context, + randomRequestCode(), // Doesn't work with direct extra Parcelable (don't know why?) // Wrap into a bundle to bypass the problem Intent(context, AutofillLauncherActivity::class.java).apply { - putExtra(KEY_SELECTION_BUNDLE, Bundle().apply { - putParcelable(KEY_SEARCH_INFO, searchInfo) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest) - } - }) + addSpecialMode(SpecialMode.SELECTION) + addSearchInfo(searchInfo) + addAutofillComponent(autofillComponent) }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT @@ -278,14 +211,17 @@ class AutofillLauncherActivity : DatabaseModeActivity() { } } - fun getPendingIntentForRegistration(context: Context, - registerInfo: RegisterInfo): PendingIntent? { + fun getPendingIntentForRegistration( + context: Context, + registerInfo: RegisterInfo + ): PendingIntent? { try { return PendingIntent.getActivity( - context, 0, + context, + randomRequestCode(), Intent(context, AutofillLauncherActivity::class.java).apply { - EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) - putExtra(KEY_REGISTER_INFO, registerInfo) + addSpecialMode(SpecialMode.REGISTRATION) + addRegisterInfo(registerInfo) }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT @@ -299,11 +235,13 @@ class AutofillLauncherActivity : DatabaseModeActivity() { } } - fun launchForRegistration(context: Context, - registerInfo: RegisterInfo) { + fun launchForRegistration( + context: Context, + registerInfo: RegisterInfo + ) { val intent = Intent(context, AutofillLauncherActivity::class.java) - EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION) - intent.putExtra(KEY_REGISTER_INFO, registerInfo) + intent.addSpecialMode(SpecialMode.REGISTRATION) + intent.addRegisterInfo(registerInfo) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt index cbcee2d74..608a1fcbb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt @@ -105,12 +105,11 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() { sharedWebDomain: String?, otpString: String?) { // Build domain search param - val searchInfo = SearchInfo().apply { - this.webDomain = sharedWebDomain - this.otpString = otpString - } - searchInfo.getConcreteWebDomain(this) { concreteWebDomain -> - searchInfo.webDomain = concreteWebDomain + getConcreteWebDomain(this, sharedWebDomain) { concreteWebDomain -> + val searchInfo = SearchInfo().apply { + this.webDomain = concreteWebDomain + this.otpString = otpString + } launch(database, searchInfo) } } @@ -212,7 +211,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() { ) } else if (searchShareForMagikeyboard) { FileDatabaseSelectActivity.launchForSelection( - activity = this, + context = this, typeMode = TypeMode.MAGIKEYBOARD, searchInfo = searchInfo ) diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt index 7f6f99c1e..e93a0f66d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt @@ -36,6 +36,8 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.activities.GroupActivity 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.addTypeMode 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.util.PasskeyHelper.addAppOrigin 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.database.ContextualDatabase import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode import com.kunzisoft.keepass.view.toastError import kotlinx.coroutines.launch import java.util.UUID @@ -121,10 +122,9 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { GroupActivity.launchForSelection( context = this@PasskeyLauncherActivity, database = uiState.database, - typeMode = TypeMode.PASSKEY, + typeMode = uiState.typeMode, activityResultLauncher = mPasskeySelectionActivityResultLauncher, - searchInfo = null, - autoSearch = false + searchInfo = uiState.searchInfo ) } is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> { @@ -138,8 +138,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { } is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> { FileDatabaseSelectActivity.launchForSelection( - activity = this@PasskeyLauncherActivity, - typeMode = TypeMode.PASSKEY, + context = this@PasskeyLauncherActivity, + typeMode = uiState.typeMode, activityResultLauncher = mPasskeySelectionActivityResultLauncher, searchInfo = uiState.searchInfo, ) @@ -276,7 +276,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { ): PendingIntent? { return PendingIntent.getActivity( context, - (Math.random() * Integer.MAX_VALUE).toInt(), + randomRequestCode(), Intent(context, PasskeyLauncherActivity::class.java).apply { addSpecialMode(specialMode) addTypeMode(TypeMode.PASSKEY) diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillComponent.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillComponent.kt index 3a4097cee..7fe7a5e37 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillComponent.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillComponent.kt @@ -2,5 +2,7 @@ package com.kunzisoft.keepass.credentialprovider.autofill import android.app.assist.AssistStructure -data class AutofillComponent(val assistStructure: AssistStructure, - val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?) \ No newline at end of file +data class AutofillComponent( + val assistStructure: AssistStructure, + val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? +) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt index 41da11367..c7b8e9b9e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.credentialprovider.autofill import android.annotation.SuppressLint -import android.app.Activity import android.app.PendingIntent import android.app.assist.AssistStructure import android.content.Context @@ -38,7 +37,6 @@ import android.view.autofill.AutofillId import android.view.autofill.AutofillManager import android.view.autofill.AutofillValue import android.widget.RemoteViews -import android.widget.Toast import android.widget.inline.InlinePresentationSpec import androidx.annotation.RequiresApi 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.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.utils.AppUtil.getConcreteWebDomain import com.kunzisoft.keepass.utils.getParcelableExtraCompat +import java.io.IOException import kotlin.math.min @RequiresApi(api = Build.VERSION_CODES.O) 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" - fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? { - intent?.getParcelableExtraCompat(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure -> + fun Intent.addAutofillComponent(autofillComponent: AutofillComponent) { + 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(EXTRA_BASE_STRUCTURE)?.let { assistStructure -> return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { AutofillComponent(assistStructure, - intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)) + getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)) } else { AutofillComponent(assistStructure, null) } @@ -127,11 +136,13 @@ object AutofillHelper { return this } - private fun buildDatasetForEntry(context: Context, - database: ContextualDatabase, - entryInfo: EntryInfo, - struct: StructureParser.Result, - inlinePresentation: InlinePresentation?): Dataset { + private fun buildDatasetForEntry( + context: Context, + database: ContextualDatabase, + entryInfo: EntryInfo, + struct: StructureParser.Result, + inlinePresentation: InlinePresentation? + ): Dataset { val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon) val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -291,11 +302,13 @@ object AutofillHelper { @SuppressLint("RestrictedApi") @RequiresApi(Build.VERSION_CODES.R) - private fun buildInlinePresentationForEntry(context: Context, - database: ContextualDatabase, - compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest, - positionItem: Int, - entryInfo: EntryInfo): InlinePresentation? { + private fun buildInlinePresentationForEntry( + context: Context, + database: ContextualDatabase, + compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest, + positionItem: Int, + entryInfo: EntryInfo + ): InlinePresentation? { compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount @@ -341,9 +354,11 @@ object AutofillHelper { @RequiresApi(Build.VERSION_CODES.R) @SuppressLint("RestrictedApi") - private fun buildInlinePresentationForManualSelection(context: Context, - inlinePresentationSpec: InlinePresentationSpec, - pendingIntent: PendingIntent): InlinePresentation? { + private fun buildInlinePresentationForManualSelection( + context: Context, + inlinePresentationSpec: InlinePresentationSpec, + pendingIntent: PendingIntent + ): InlinePresentation? { // Make sure that the IME spec claims support for v1 UI template. val imeStyle = inlinePresentationSpec.style if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) @@ -360,11 +375,14 @@ object AutofillHelper { }.build().slice, inlinePresentationSpec, false) } - fun buildResponse(context: Context, - database: ContextualDatabase, - entriesInfo: List, - parseResult: StructureParser.Result, - compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? { + fun buildResponse( + context: Context, + database: ContextualDatabase, + entriesInfo: List, + parseResult: StructureParser.Result, + concreteWebDomain: String?, + autofillComponent: AutofillComponent + ): FillResponse? { val responseBuilder = FillResponse.Builder() // Add Header if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -385,7 +403,8 @@ object AutofillHelper { // Add inline suggestion for new IME and dataset var numberInlineSuggestions = 0 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> + autofillComponent.compatInlineSuggestionsRequest + ?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size) if (PreferencesUtil.isAutofillManualSelectionEnable(context)) { if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) { @@ -401,21 +420,27 @@ object AutofillHelper { var inlinePresentation: InlinePresentation? = null if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && numberInlineSuggestions > 0 - && compatInlineSuggestionsRequest != null) { + && autofillComponent.compatInlineSuggestionsRequest != null) { inlinePresentation = buildInlinePresentationForEntry( context, database, - compatInlineSuggestionsRequest, + autofillComponent.compatInlineSuggestionsRequest, numberInlineSuggestions--, entry ) } // Create dataset for each entry responseBuilder.addDataset( - buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation) + buildDatasetForEntry( + context = context, + database = database, + entryInfo = entry, + struct = parseResult, + inlinePresentation = inlinePresentation + ) ) } 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)) { val searchInfo = SearchInfo().apply { applicationId = parseResult.applicationId - webDomain = parseResult.webDomain + webDomain = concreteWebDomain webScheme = parseResult.webScheme manualSelection = true } - val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry) - AutofillLauncherActivity.getPendingIntentForSelection(context, - searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent -> + val manualSelectionView = RemoteViews( + context.packageName, + R.layout.item_autofill_select_entry + ) + AutofillLauncherActivity.getPendingIntentForSelection( + context, + searchInfo, + autofillComponent + )?.let { pendingIntent -> var inlinePresentation: InlinePresentation? = null if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> - val inlinePresentationSpec = - inlineSuggestionsRequest.inlinePresentationSpecs[0] - inlinePresentation = buildInlinePresentationForManualSelection( - context, - inlinePresentationSpec, - pendingIntent - ) - } + autofillComponent.compatInlineSuggestionsRequest + ?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> + val inlinePresentationSpec = + inlineSuggestionsRequest.inlinePresentationSpecs[0] + inlinePresentation = buildInlinePresentationForManualSelection( + context, + inlinePresentationSpec, + pendingIntent + ) + } } 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, - database: ContextualDatabase, - entryInfo: EntryInfo) { - buildResponseAndSetResult(activity, database, ArrayList().apply { add(entryInfo) }) - } - - /** - * Build the Autofill response for many entry - */ - fun buildResponseAndSetResult(activity: Activity, - database: ContextualDatabase, - entriesInfo: List) { + fun buildResponse( + context: Context, + autofillComponent: AutofillComponent, + database: ContextualDatabase, + entriesInfo: List, + onIntentCreated: suspend (Intent) -> Unit + ) { if (entriesInfo.isEmpty()) { - activity.setResult(Activity.RESULT_CANCELED) + throw IOException("No entries found") } else { - var setResultOk = false - activity.intent?.getParcelableExtraCompat(EXTRA_ASSIST_STRUCTURE)?.let { structure -> - StructureParser(structure).parse()?.let { result -> + StructureParser(autofillComponent.assistStructure).parse()?.let { result -> + getConcreteWebDomain(context, result.webDomain) { concreteWebDomain -> // New Response - val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat( - EXTRA_INLINE_SUGGESTIONS_REQUEST + onIntentCreated(Intent().putExtra( + AutofillManager.EXTRA_AUTHENTICATION_RESULT, + 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) + )) } - } - 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) - } + } ?: throw IOException("Unable to parse the structure") } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt index d07211717..1816e989a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt @@ -92,10 +92,11 @@ class KeeAutofillService : AutofillService() { autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this) } - override fun onFillRequest(request: FillRequest, - cancellationSignal: CancellationSignal, - callback: FillCallback) { - + override fun onFillRequest( + request: FillRequest, + cancellationSignal: CancellationSignal, + callback: FillCallback + ) { cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") } if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) { @@ -115,13 +116,12 @@ class KeeAutofillService : AutofillService() { webDomain = parseResult.webDomain, webDomainBlocklist = webDomainBlocklist) ) { - val searchInfo = SearchInfo().apply { - applicationId = parseResult.applicationId - webDomain = parseResult.webDomain - webScheme = parseResult.webScheme - } - searchInfo.getConcreteWebDomain(this) { webDomainWithoutSubDomain -> - searchInfo.webDomain = webDomainWithoutSubDomain + getConcreteWebDomain(this, parseResult.webDomain) { webDomainWithoutSubDomain -> + val searchInfo = SearchInfo().apply { + applicationId = parseResult.applicationId + webDomain = webDomainWithoutSubDomain + webScheme = parseResult.webScheme + } val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && autofillInlineSuggestionsEnabled) { CompatInlineSuggestionsRequest(request) @@ -129,20 +129,26 @@ class KeeAutofillService : AutofillService() { null } launchSelection(mDatabase, - searchInfo, - parseResult, - inlineSuggestionsRequest, - callback) + searchInfo, + parseResult, + AutofillComponent( + latestStructure, + inlineSuggestionsRequest + ), + callback + ) } } } } - private fun launchSelection(database: ContextualDatabase?, - searchInfo: SearchInfo, - parseResult: StructureParser.Result, - inlineSuggestionsRequest: CompatInlineSuggestionsRequest?, - callback: FillCallback) { + private fun launchSelection( + database: ContextualDatabase?, + searchInfo: SearchInfo, + parseResult: StructureParser.Result, + autofillComponent: AutofillComponent, + callback: FillCallback + ) { SearchHelper.checkAutoSearchInfo( context = this, database = database, @@ -150,37 +156,46 @@ class KeeAutofillService : AutofillService() { onItemsFound = { openedDatabase, items -> callback.onSuccess( AutofillHelper.buildResponse( - this, openedDatabase, - items, parseResult, inlineSuggestionsRequest + context = this, + database = openedDatabase, + entriesInfo = items, + parseResult = parseResult, + concreteWebDomain = searchInfo.webDomain, + autofillComponent = autofillComponent ) ) }, onItemNotFound = { openedDatabase -> // Show UI if no search result showUIForEntrySelection(parseResult, openedDatabase, - searchInfo, inlineSuggestionsRequest, callback) + searchInfo, autofillComponent, callback) }, onDatabaseClosed = { // Show UI if database not open showUIForEntrySelection(parseResult, null, - searchInfo, inlineSuggestionsRequest, callback) + searchInfo, autofillComponent, callback) } ) } @SuppressLint("RestrictedApi") - private fun showUIForEntrySelection(parseResult: StructureParser.Result, - database: ContextualDatabase?, - searchInfo: SearchInfo, - inlineSuggestionsRequest: CompatInlineSuggestionsRequest?, - callback: FillCallback) { + private fun showUIForEntrySelection( + parseResult: StructureParser.Result, + database: ContextualDatabase?, + searchInfo: SearchInfo, + autofillComponent: AutofillComponent, + callback: FillCallback + ) { var success = false parseResult.allAutofillIds().let { autofillIds -> if (autofillIds.isNotEmpty()) { // If the entire Autofill Response is authenticated, AuthActivity is used // to generate Response. - AutofillLauncherActivity.getPendingIntentForSelection(this, - searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender -> + AutofillLauncherActivity.getPendingIntentForSelection( + this, + searchInfo, + autofillComponent + )?.intentSender?.let { intentSender -> val responseBuilder = FillResponse.Builder() val remoteViewsUnlock: RemoteViews = if (database == null) { if (!parseResult.webDomain.isNullOrEmpty()) { @@ -271,7 +286,8 @@ class KeeAutofillService : AutofillService() { && autofillInlineSuggestionsEnabled ) { var inlinePresentation: InlinePresentation? = null - inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> + autofillComponent.compatInlineSuggestionsRequest + ?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs if (inlineSuggestionsRequest.maxSuggestionCount > 0 @@ -387,33 +403,40 @@ class KeeAutofillService : AutofillService() { } // Show UI to save data - val registerInfo = RegisterInfo( - searchInfo = SearchInfo().apply { + getConcreteWebDomain(applicationContext, parseResult.webDomain) { concreteWebDomain -> + val searchInfo = SearchInfo().apply { applicationId = parseResult.applicationId - webDomain = parseResult.webDomain + webDomain = concreteWebDomain 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) { - // callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this, - // registerInfo)) - //} else { - AutofillLauncherActivity.launchForRegistration(this, registerInfo) - success = true - callback.onSuccess() - //} + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // TODO Test pending intent + AutofillLauncherActivity.getPendingIntentForRegistration( + this, + registerInfo + )?.intentSender?.let { intentSender -> + callback.onSuccess(intentSender) + } ?: callback.onFailure("Unable to launch registration") + } else { + AutofillLauncherActivity.launchForRegistration(this, registerInfo) + success = true + callback.onSuccess() + } + } } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/MagikeyboardService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/MagikeyboardService.kt index 459d7a6a8..875609bd4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/MagikeyboardService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/MagikeyboardService.kt @@ -43,6 +43,7 @@ import androidx.recyclerview.widget.RecyclerView import com.kunzisoft.keepass.R import com.kunzisoft.keepass.adapters.FieldsAdapter import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper +import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.DatabaseTaskProvider @@ -484,7 +485,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL // Populate Magikeyboard with entry addEntryAndLaunchNotificationIfAllowed(activity, entry, toast) // Consume the selection mode - EntrySelectionHelper.removeModesFromIntent(activity.intent) + activity.intent.removeModes() activity.moveTaskToBack(true) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt index a556fc562..48a532d23 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt @@ -24,7 +24,6 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle -import android.os.ParcelUuid import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties 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.getApplicationFingerprints 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.AuthenticatorAttestationResponse 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.EntryInfo import com.kunzisoft.keepass.model.Passkey -import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.utils.AppUtil import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.getParcelableExtraCompat @@ -88,10 +87,7 @@ object PasskeyHelper { 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_NODE_ID = "com.kunzisoft.keepass.extra.nodeId" private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp" private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode" @@ -110,38 +106,6 @@ object PasskeyHelper { 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 */ @@ -181,22 +145,6 @@ object PasskeyHelper { 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 */ @@ -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?) { - nodeId?.let { - putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId)) + 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) } } - /** - * Retrieve the node id from the intent - */ - fun Intent.retrieveNodeId(): UUID? { - return getParcelableExtraCompat(EXTRA_NODE_ID)?.uuid - } - /** * Check the timestamp and authentication code transmitted via PendingIntent */ diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/AutofillLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/AutofillLauncherViewModel.kt new file mode 100644 index 000000000..b9c156d34 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/AutofillLauncherViewModel.kt @@ -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.Loading) + val uiState: StateFlow = 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt new file mode 100644 index 000000000..a48d4a4c3 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt @@ -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.Loading) + val credentialUiState: StateFlow = 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt index e3e7e4319..a0fd93829 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt @@ -13,6 +13,8 @@ import androidx.credentials.exceptions.GetCredentialUnknownException import androidx.credentials.provider.PendingIntentHandler import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId +import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.TypeMode 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.removePasskey 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.retrievePasskeyCreationRequestParameters 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.saveCustomPrivilegedApps import com.kunzisoft.keepass.database.ContextualDatabase @@ -261,14 +261,17 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli "launch manual selection in opened database" ) _uiState.value = UIState.LaunchGroupActivityForSelection( - database = openedDatabase + database = openedDatabase, + searchInfo = searchInfo, + typeMode = TypeMode.PASSKEY ) }, onDatabaseClosed = { Log.d(TAG, "Manual passkey selection in closed database") _uiState.value = UIState.LaunchFileDatabaseSelectActivityForSelection( - searchInfo = searchInfo + searchInfo = searchInfo, + typeMode = TypeMode.PASSKEY ) } ) @@ -550,7 +553,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli val nodeId: UUID ): UIState() data class LaunchGroupActivityForSelection( - val database: ContextualDatabase + val database: ContextualDatabase, + val searchInfo: SearchInfo?, + val typeMode: TypeMode ): UIState() data class LaunchGroupActivityForRegistration( val database: ContextualDatabase, @@ -558,7 +563,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli val typeMode: TypeMode ): UIState() data class LaunchFileDatabaseSelectActivityForSelection( - val searchInfo: SearchInfo + val searchInfo: SearchInfo, + val typeMode: TypeMode ): UIState() data class LaunchFileDatabaseSelectActivityForRegistration( val registerInfo: RegisterInfo, diff --git a/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt index 277f796f3..8a95d3949 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt @@ -54,6 +54,7 @@ object SearchHelper { onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit, onDatabaseClosed: () -> Unit ) { + // TODO suspend if (database == null || !database.loaded) { onDatabaseClosed.invoke() } else if (TimeoutHelper.checkTime(context)) { diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt index 6e7bd66d3..855174944 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt @@ -23,6 +23,10 @@ import mozilla.components.lib.publicsuffixlist.PublicSuffixList object AppUtil { + fun randomRequestCode(): Int { + return (Math.random() * Integer.MAX_VALUE).toInt() + } + fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean { try { this.applicationContext.packageManager.getPackageInfoCompat( @@ -83,9 +87,10 @@ object AppUtil { /** * Get the concrete web domain AKA without sub domain if needed */ - fun SearchInfo.getConcreteWebDomain( + fun getConcreteWebDomain( context: Context, - concreteWebDomain: (String?) -> Unit + webDomain: String?, + concreteWebDomain: suspend (String?) -> Unit ) { CoroutineScope(Dispatchers.IO).launch { val domain = webDomain diff --git a/database/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt b/database/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt index 6d0f0a487..94d3a5e71 100644 --- a/database/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt +++ b/database/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt @@ -39,7 +39,7 @@ inline fun Intent.getSerializableExtraCompat(key: Str else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T } -inline fun Intent.putParcelableList(key: String?, list: MutableList) { +inline fun Intent.putParcelableList(key: String?, list: List) { putExtra(key, list.toTypedArray()) }