fix: Autofill refactoring

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

View File

@@ -56,9 +56,10 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.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<Intent>? = null,
) {
if (database.loaded && !database.isReadOnly) {
@@ -868,7 +870,6 @@ class EntryEditActivity : DatabaseLockActivity(),
intent = intent,
typeMode = typeMode,
searchInfo = searchInfo,
autofillComponent = autofillComponent,
activityResultLauncher = activityResultLauncher
)
}

View File

@@ -50,7 +50,6 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.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<Intent>? = 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
)
}

View File

@@ -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<Intent>? = 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
)

View File

@@ -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<Intent>? = null,
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
@@ -824,7 +822,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
intent = intent,
typeMode = typeMode,
searchInfo = searchInfo,
autofillComponent = autofillComponent,
activityResultLauncher = activityResultLauncher
)
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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<Intent>? = 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<EntryInfo>,
extras: Bundle? = null
) {
try {
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success special mode manual selection")
mReplyIntent.addNodesIds(entriesInfo.map { it.id })
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the result", e)
setResult(Activity.RESULT_CANCELED)
}
}
fun Intent.addSearchInfo(searchInfo: SearchInfo?): Intent {
searchInfo?.let {
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<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
fun Intent.retrieveSpecialMode(): SpecialMode {
return getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
}
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
// TODO Replace by Intent.addTypeMode
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
}
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
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<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
fun Intent.retrieveTypeMode(): TypeMode {
return getEnumExtra<TypeMode>(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<UUID>): Intent {
this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
return this
}
fun Intent.retrieveNodesIds(): List<UUID>? {
return getParcelableList<ParcelUuid>(EXTRA_NODES_IDS)?.map { it.uuid }
}
fun Intent.removeNodesIds() {
removeExtra(EXTRA_NODES_IDS)
}
/**
* Add the node id to the intent
*/
fun Intent.addNodeId(nodeId: UUID?) {
nodeId?.let {
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
}
}
/**
* Retrieve the node id from the intent
*/
fun Intent.retrieveNodeId(): UUID? {
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
}
/**
@@ -221,9 +265,8 @@ object EntrySelectionHelper {
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
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<SpecialMode>(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<SpecialMode>(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(

View File

@@ -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<Intent>? =
this.buildActivityResultLauncher(typeMode = TypeMode.AUTOFILL, lockDatabase = true)
private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageSelectionResult(it)
}
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageRegistrationResult(it)
}
override fun applyCustomStyle(): Boolean {
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<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
searchInfo.getConcreteWebDomain(this) { concreteWebDomain ->
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper
.retrieveAutofillComponent(intent)
?.assistStructure
val newAutofillComponent = if (assistStructure != null) {
AutofillComponent(
assistStructure,
compatInlineSuggestionsRequest
)
} else {
null
}
searchInfo.webDomain = concreteWebDomain
launchSelection(database, newAutofillComponent, searchInfo)
}
}
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<RegisterInfo>(
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)
}

View File

@@ -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
)

View File

@@ -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)

View File

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

View File

@@ -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<AssistStructure>(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<AssistStructure>(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<EntryInfo>,
parseResult: StructureParser.Result,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
fun buildResponse(
context: Context,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>,
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<EntryInfo>().apply { add(entryInfo) })
}
/**
* Build the Autofill response for many entry
*/
fun buildResponseAndSetResult(activity: Activity,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>) {
fun buildResponse(
context: Context,
autofillComponent: AutofillComponent,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>,
onIntentCreated: suspend (Intent) -> Unit
) {
if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED)
throw IOException("No entries found")
} else {
var setResultOk = false
activity.intent?.getParcelableExtraCompat<AssistStructure>(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<CompatInlineSuggestionsRequest>(
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")
}
}

View File

@@ -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()
}
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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<ParcelUuid>(EXTRA_NODE_ID)?.uuid
}
/**
* Check the timestamp and authentication code transmitted via PendingIntent
*/

View File

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

View File

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

View File

@@ -13,6 +13,8 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import androidx.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,

View File

@@ -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)) {

View File

@@ -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

View File

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