Compare commits

...

82 Commits

Author SHA1 Message Date
J-Jamet
208f1e97d5 Merge branch 'release/4.2.0' 2025-10-15 23:30:40 +02:00
J-Jamet
e4e0628e20 fix: Upgrade CHANGELOG 2025-10-15 23:22:41 +02:00
J-Jamet
f60f31771f fix: Upgrade to 4.2.0 2025-10-15 23:16:45 +02:00
J-Jamet
ff6367bac4 fix: Change screens 2025-10-15 23:08:16 +02:00
J-Jamet
540e72812e fix: Passkey browser signature #2213 2025-10-15 21:15:10 +02:00
J-Jamet
5fe4af8e9d fix: Passkey multiple instance #2215 2025-10-15 20:09:27 +02:00
J-Jamet
ae42ab43b7 fix: Passkey multiple instance #2215 2025-10-15 19:37:56 +02:00
J-Jamet
c463055971 fix: Passkey back #2215 2025-10-15 15:46:26 +02:00
J-Jamet
1849dca81d fix: Form filling auto search #2204 2025-10-15 15:14:30 +02:00
J-Jamet
b3dd3dcfb5 fix: Toast "Usage parameter is null"
#2214
2025-10-14 15:36:08 +02:00
J-Jamet
fef88ff270 feat: Add KPEX_PASSKEY_FLAG_BE and KPEX_PASSKEY_FLAG_BS flags #2212 2025-10-14 14:21:52 +02:00
J-Jamet
f1f7dd1e6c fix: Upgrade to 4.2.0beta04 2025-10-14 14:12:01 +02:00
J-Jamet
96c3af097a fix: Callback after registration 2025-10-13 14:57:13 +02:00
J-Jamet
4fe6b2e115 fix: Multiple validation 2025-10-13 11:48:12 +02:00
J-Jamet
cc936b9304 fix: Database Info 2025-10-13 11:08:56 +02:00
J-Jamet
e7f2a22583 fix: Hardware key dialog 2025-10-13 10:52:25 +02:00
J-Jamet
4bf905ecda fix: Hardware key #2196 2025-10-13 10:33:04 +02:00
J-Jamet
f8d80525d9 fix: Hardware key #2196 2025-10-13 10:32:43 +02:00
J-Jamet
65857596a6 fix: Multiple launch instance 2025-10-10 13:52:08 +02:00
J-Jamet
e6253336bd fix: Remove intent data 2025-10-09 20:29:46 +02:00
J-Jamet
e5595a3275 fix: Database workflow 2025-10-09 19:41:36 +02:00
J-Jamet
366e8bf1d7 fix: IconCompat 2025-10-09 18:39:59 +02:00
J-Jamet
fa63265599 fix: null database 2025-10-09 15:13:58 +02:00
J-Jamet
755e0ea9a5 fix: Database State 2025-10-09 14:31:03 +02:00
J-Jamet
50b1ac388e fix: Complete refactoring of database action 2025-10-08 15:53:41 +02:00
J-Jamet
51c62034df fix: Refactoring database as flow 2025-10-08 12:11:41 +02:00
J-Jamet
e4d0cd89c6 fix: Small refactoring 2025-10-08 11:40:25 +02:00
J-Jamet
e085d5d277 fix: Remove unused code 2025-10-07 13:56:42 +02:00
J-Jamet
05336e93a0 fix: Special Characters #2180 2025-10-07 13:56:27 +02:00
J-Jamet
90b3b56893 feat: Warning when overwriting existing passkey #2124 2025-10-07 13:27:46 +02:00
J-Jamet
02c514272e feat: OTP tag #2122 2025-10-07 11:56:14 +02:00
J-Jamet
989e47ed12 Merge branch 'develop' into release/4.2.0 2025-10-06 17:38:54 +02:00
J-Jamet
1caf132558 Merge tag '4.1.9' into develop
4.1.9
2025-10-06 17:27:35 +02:00
J-Jamet
1b98bd740c Merge branch 'release/4.1.9' 2025-10-06 17:27:26 +02:00
J-Jamet
5adeb5cde0 fix: Tags 2025-10-06 17:20:59 +02:00
J-Jamet
b949d5d861 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-06 17:18:20 +02:00
J-Jamet
b4264a30a4 fix: Update description 2025-10-06 17:16:57 +02:00
J-Jamet
cf799c0f68 fix: Update to 4.1.9 2025-10-06 17:14:10 +02:00
J-Jamet
97f0ca519b fix: Killed service #2201 2025-10-06 16:59:42 +02:00
J-Jamet
cf4047b701 Merge branch 'chenxiaolong-landscape-insets' into develop 2025-10-06 13:58:39 +02:00
J-Jamet
40608a3eb5 Merge branch 'landscape-insets' of github.com:chenxiaolong/KeePassDX into chenxiaolong-landscape-insets 2025-10-06 13:58:20 +02:00
J-Jamet
99cb50d031 fix: Bug report title 2025-10-06 12:47:30 +02:00
Oğuz Ersen
b0d0c35241 Translated using Weblate (Turkish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/tr/
2025-10-05 18:02:07 +00:00
Mekyla Credo
6044c93a4a Translated using Weblate (Filipino)
Currently translated at 46.5% (309 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2025-10-05 18:02:05 +00:00
Oğuz Ersen
b544b5d54d Translated using Weblate (Turkish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-10-05 18:02:02 +00:00
Andrew Gunnerson
852378e484 Simplify inset logic, fix landscape mode, fix cutout overlapping
The commit primarily fixes a few overlapping issues caused by the window
inset handling. Previously, there were two main issues:

* Because setTransparentNavigationBar() checked for portrait mode, the
  inset logic never executed in landscape mode. This caused the app to
  overlap the status bar and navigation bar.

* The inset logic did not have handling for displayCutout insets. In
  landscape mode, this would cause the app to overlap the notch or
  camera hole punch area on phones.

In addition to fixing those issues, this commit simplifies the inset
logic a bit:

* applyWindowInsets() now accepts an EnumSet of WindowInsetPosition to
  avoid needing to duplicate logic for the various position
  combinations.

* Insets are now applied to the main container in the layout instead of
  individual elements where possible. This eliminates the need for the
  previous manual IME height handling logic in BOTTOM_IME vs
  TOP_BOTTOM_IME (for avoiding the insets being applied twice).

* Since insets are now applied to the main layout container,
  applyWindowInsets() now takes systemBars, displayCutout, and ime all
  into consideration. This should avoid all possible overlapping.

* Add support for using padding instead of margins for insets. This is
  used for GroupActivity's navigation drawer, where Material design
  intends for the drawer background to be drawn behind system bars.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-10-04 17:24:24 -04:00
Alonso González Chaves
711a344860 Translated using Weblate (Spanish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/es/
2025-10-04 14:02:05 +02:00
Alonso González Chaves
72087c7e5c Translated using Weblate (Spanish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-10-04 14:02:01 +02:00
J-Jamet
a337de3679 fix: Small refactoring 2025-10-01 15:41:49 +02:00
J-Jamet
75b37f5a9f fix: Settings 2025-09-30 15:29:10 +02:00
J-Jamet
075f54b286 feat: passkey selection after close database setting #2187 2025-09-30 15:19:05 +02:00
J-Jamet
e07cbc2e14 fix: Default backup state 2025-09-30 13:19:25 +02:00
J-Jamet
ac29b7bac7 fix: Launch immediately 2025-09-30 13:19:07 +02:00
J-Jamet
b9129cb941 fix: Update CHANGELOG 2025-09-29 19:40:28 +02:00
J-Jamet
6957fcd81a fix: Small bugs 2025-09-29 18:39:14 +02:00
J-Jamet
cfe56fc055 fix: Small error 2025-09-29 00:04:19 +02:00
J-Jamet
6f3e065ad1 fix: Back UI 2025-09-28 23:49:08 +02:00
J-Jamet
abfa7a3f47 fix: Registration and webDomain coroutine 2025-09-28 23:24:37 +02:00
J-Jamet
dd0d85e54e fix: Autofill refactoring 2025-09-26 21:42:22 +02:00
J-Jamet
76c20263f7 fix: Refactoring type mode call 2025-09-25 20:50:14 +02:00
J-Jamet
e447388611 fix: Refactoring activity launcher 2025-09-25 16:32:25 +02:00
J-Jamet
1bfec67c02 fix: Empty save parameter 2025-09-25 14:26:15 +02:00
J-Jamet
45041216d6 fix: Autofill recognition and ask to save disabled by default 2025-09-25 13:54:01 +02:00
J-Jamet
e075e9018c fix: Refactoring result launcher 2025-09-25 12:42:40 +02:00
J-Jamet
eed304ec40 fix: Search settings #2112 #2181 2025-09-24 22:36:50 +02:00
J-Jamet
5bcbbac97f fix: Save mode as registration mode 2025-09-24 19:25:24 +02:00
J-Jamet
ea4750fc11 fix: Change store description 2025-09-24 16:43:44 +02:00
J-Jamet
5037821529 Revert "fix: Backup eligibility default #2150"
This reverts commit 1d4e1687cf.
2025-09-24 14:33:43 +02:00
J-Jamet
3a4c88f19a fix: Empty database name #2159 2025-09-24 14:27:57 +02:00
J-Jamet
e960a8e169 fix: Upgrade to 4.2.0beta03 2025-09-24 14:14:29 +02:00
J-Jamet
1d4e1687cf fix: Backup eligibility default #2150 2025-09-24 14:13:49 +02:00
J-Jamet
033fa95285 Merge branch 'develop' into release/4.2.0 2025-09-24 13:21:22 +02:00
J-Jamet
f17d211fbd fix: Change appearance summary #2171 2025-09-24 13:19:57 +02:00
J-Jamet
cae69e7572 fix: Update CHANGELOG 2025-09-18 21:05:26 +02:00
Xo
ae903ad236 Translated using Weblate (Hebrew)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-09-15 09:02:00 +00:00
Milo Ivir
7c3a15ce79 Translated using Weblate (Croatian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2025-09-12 16:01:59 +00:00
shinebrillant
b609d4e182 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/zh_Hant/
2025-09-11 13:02:01 +00:00
Artyom Rybakov
e8ecf28f7c Translated using Weblate (Russian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-09-10 14:21:47 +02:00
shinebrillant
3d5adbfc01 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2025-09-10 14:21:46 +02:00
Matthaiks
72bfc50703 Translated using Weblate (Polish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-09-10 14:21:46 +02:00
Matthaiks
a60e2e780d Translated using Weblate (Polish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-09-09 12:01:58 +00:00
Retrial
9210851765 Translated using Weblate (Greek)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2025-09-09 12:01:56 +00:00
181 changed files with 4299 additions and 3569 deletions

View File

@@ -1,4 +1,4 @@
name: Bug Report name: Bug report
description: Report a bug. description: Report a bug.
labels: ["bug"] labels: ["bug"]
body: body:

View File

@@ -1,5 +1,17 @@
KeePassDX(4.2.0) KeePassDX(4.2.0)
* Passkeys management #1421 #2097 (Thx @cali-95) * Passkeys management #1421 #2097 (@cali-95)
* Confirm usage of passkey #2165 #2124
* Dialog to manage missing signature #2152 #2155 #2161 #2160
* Capture error #2159 #2215
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
* Search settings #2112 #2181 #2187 #2204
* Autofill refactoring #765 #2196
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
KeePassDX(4.1.9)
* Fix landscape UI #2198 #2200 (@chenxiaolong)
* Fix start loop and flash screen #2201
* Small fixes
KeePassDX(4.1.8) KeePassDX(4.1.8)
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP) * Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 35 targetSdkVersion 35
versionCode = 142 versionCode = 145
versionName = "4.2.0beta02" versionName = "4.2.0"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -178,18 +178,22 @@
<activity <activity
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
<activity <activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
android:theme="@style/Theme.Transparent" /> android:theme="@style/Theme.Transparent"
android:exported="false"
android:excludeFromRecents="true" />
<activity <activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:excludeFromRecents="true"/> android:exported="false"
android:excludeFromRecents="true" />
<activity <activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:exported="true"> android:exported="true"
android:excludeFromRecents="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@@ -208,8 +212,8 @@
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:excludeFromRecents="true"
android:exported="false" android:exported="false"
android:excludeFromRecents="true"
tools:targetApi="upside_down_cake" /> tools:targetApi="upside_down_cake" />
<service <service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService" android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"

View File

@@ -79,11 +79,13 @@ import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.EnumSet
import java.util.UUID import java.util.UUID
class EntryActivity : DatabaseLockActivity() { class EntryActivity : DatabaseLockActivity() {
private var footer: ViewGroup? = null private var footer: ViewGroup? = null
private var container: View? = null
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null private var appBarLayout: AppBarLayout? = null
@@ -135,6 +137,7 @@ class EntryActivity : DatabaseLockActivity() {
// Get views // Get views
footer = findViewById(R.id.activity_entry_footer) footer = findViewById(R.id.activity_entry_footer)
container = findViewById(R.id.activity_entry_container)
coordinatorLayout = findViewById(R.id.toolbar_coordinator) coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout) collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar) appBarLayout = findViewById(R.id.app_bar)
@@ -150,8 +153,12 @@ class EntryActivity : DatabaseLockActivity() {
setTransparentNavigationBar { setTransparentNavigationBar {
// To fix margin with API 27 // To fix margin with API 27
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null) ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP) container?.applyWindowInsets(EnumSet.of(
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM) WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
} }
// Empty title // Empty title
@@ -305,11 +312,11 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.historySelected.observe(this) { historySelected -> mEntryViewModel.historySelected.observe(this) { historySelected ->
mDatabase?.let { database -> mDatabase?.let { database ->
launch( launch(
this, activity = this,
database, database = database,
historySelected.nodeId, entryId = historySelected.nodeId,
historySelected.historyPosition, historyPosition = historySelected.historyPosition,
mEntryActivityResultLauncher activityResultLauncher = mEntryActivityResultLauncher
) )
} }
} }
@@ -323,9 +330,8 @@ class EntryActivity : DatabaseLockActivity() {
return coordinatorLayout return coordinatorLayout
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database) mEntryViewModel.loadDatabase(database)
} }
@@ -471,11 +477,12 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_edit -> { R.id.menu_edit -> {
mDatabase?.let { database -> mDatabase?.let { database ->
mMainEntryId?.let { entryId -> mMainEntryId?.let { entryId ->
EntryEditActivity.launchToUpdate( EntryEditActivity.launch(
this, activity = this,
database, database = database,
entryId, registrationType = EntryEditActivity.RegistrationType.UPDATE,
mEntryActivityResultLauncher nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
) )
} }
} }
@@ -513,7 +520,7 @@ class EntryActivity : DatabaseLockActivity() {
// Transit data in previous Activity after an update // Transit data in previous Activity after an update
Intent().apply { Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId) putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(Activity.RESULT_OK, this) setResult(RESULT_OK, this)
} }
super.finish() super.finish()
} }
@@ -527,34 +534,22 @@ class EntryActivity : DatabaseLockActivity() {
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG" const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
/** /**
* Open standard Entry activity * Open standard or history Entry activity
*/ */
fun launch(activity: Activity, fun launch(
database: ContextualDatabase, activity: Activity,
entryId: NodeId<UUID>, database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>) { entryId: NodeId<UUID>,
historyPosition: Int? = null,
activityResultLauncher: ActivityResultLauncher<Intent>
) {
if (database.loaded) { if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java) val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId) intent.putExtra(KEY_ENTRY, entryId)
activityResultLauncher.launch(intent) historyPosition?.let {
} intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
} }
}
/**
* Open history Entry activity
*/
fun launch(activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
historyPosition: Int,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
activityResultLauncher.launch(intent) activityResultLauncher.launch(intent)
} }
} }

View File

@@ -36,14 +36,14 @@ import android.widget.Spinner
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.MaterialTimePicker
@@ -59,9 +59,10 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
@@ -101,6 +102,8 @@ import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingStart import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import kotlinx.coroutines.launch
import java.util.EnumSet
import java.util.UUID import java.util.UUID
class EntryEditActivity : DatabaseLockActivity(), class EntryEditActivity : DatabaseLockActivity(),
@@ -155,9 +158,6 @@ class EntryEditActivity : DatabaseLockActivity(),
} }
} }
// To ask data lost only one time
private var backPressedAlreadyApproved = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry_edit) setContentView(R.layout.activity_entry_edit)
@@ -181,8 +181,12 @@ class EntryEditActivity : DatabaseLockActivity(),
// To apply fit window with transparency // To apply fit window with transparency
setTransparentNavigationBar(applyToStatusBar = true) { setTransparentNavigationBar(applyToStatusBar = true) {
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME) container?.applyWindowInsets(EnumSet.of(
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME) WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
} }
stopService(Intent(this, ClipboardEntryNotificationService::class.java)) stopService(Intent(this, ClipboardEntryNotificationService::class.java))
@@ -206,8 +210,8 @@ class EntryEditActivity : DatabaseLockActivity(),
mDatabase, mDatabase,
entryId, entryId,
parentId, parentId,
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent), intent.retrieveRegisterInfo()
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) ?: intent.retrieveSearchInfo()?.toRegisterInfo()
) )
// To retrieve attachment // To retrieve attachment
@@ -374,30 +378,30 @@ class EntryEditActivity : DatabaseLockActivity(),
} ?: run { } ?: run {
updateEntry(entrySave.oldEntry, entrySave.newEntry) updateEntry(entrySave.oldEntry, entrySave.newEntry)
} }
}
// Don't wait for saving if it's to provide autofill lifecycleScope.launch {
mDatabase?.let { database -> repeatOnLifecycle(Lifecycle.State.STARTED) {
EntrySelectionHelper.doSpecialAction( mEntryEditViewModel.uiState.collect { uiState ->
intent = intent, when (uiState) {
defaultAction = {}, EntryEditViewModel.UIState.Loading -> {}
searchAction = {}, EntryEditViewModel.UIState.ShowOverwriteMessage -> {
saveAction = {}, if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
keyboardSelectionAction = { AlertDialog.Builder(this@EntryEditActivity)
entryValidatedForKeyboardSelection(database, entrySave.newEntry) .setTitle(R.string.warning_overwrite_data_title)
}, .setMessage(R.string.warning_overwrite_data_description)
autofillSelectionAction = { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
entryValidatedForAutofillSelection(database, entrySave.newEntry) mEntryEditViewModel.backPressedAlreadyApproved = true
}, onCancelSpecialMode()
autofillRegistrationAction = { }
entryValidatedForAutofillRegistration(entrySave.newEntry) .setPositiveButton(android.R.string.ok) { _, _ ->
}, mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
passkeySelectionAction = { }
entryValidatedForPasskeySelection(database, entrySave.newEntry) .create().show()
}, }
passkeyRegistrationAction = { }
entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
} }
) }
} }
} }
} }
@@ -410,13 +414,13 @@ class EntryEditActivity : DatabaseLockActivity(),
return true return true
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mAllowCustomFields = database?.allowEntryCustomFields() == true mAllowCustomFields = database.allowEntryCustomFields() == true
mAllowOTP = database?.allowOTP == true mAllowOTP = database.allowOTP == true
mEntryEditViewModel.loadDatabase(database) mEntryEditViewModel.loadTemplateEntry(database)
mTemplatesSelectorAdapter?.apply { mTemplatesSelectorAdapter?.apply {
iconDrawableFactory = mDatabase?.iconDrawableFactory iconDrawableFactory = database.iconDrawableFactory
notifyDataSetChanged() notifyDataSetChanged()
} }
} }
@@ -427,6 +431,7 @@ class EntryEditActivity : DatabaseLockActivity(),
result: ActionRunnable.Result result: ActionRunnable.Result
) { ) {
super.onDatabaseActionFinished(database, actionTask, result) super.onDatabaseActionFinished(database, actionTask, result)
mEntryEditViewModel.unlockAction()
when (actionTask) { when (actionTask) {
ACTION_DATABASE_CREATE_ENTRY_TASK, ACTION_DATABASE_CREATE_ENTRY_TASK,
ACTION_DATABASE_UPDATE_ENTRY_TASK -> { ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
@@ -442,23 +447,27 @@ class EntryEditActivity : DatabaseLockActivity(),
searchAction = { searchAction = {
// Nothing when search retrieved // Nothing when search retrieved
}, },
saveAction = { selectionAction = { intentSender, typeMode, searchInfo ->
entryValidatedForSave(entry) when(typeMode) {
TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD ->
entryValidatedForKeyboardSelection(database, entry)
TypeMode.PASSKEY ->
entryValidatedForPasskey(database, entry)
TypeMode.AUTOFILL ->
entryValidatedForAutofill(database, entry)
}
}, },
keyboardSelectionAction = { registrationAction = { _, typeMode, _ ->
entryValidatedForKeyboardSelection(database, entry) when(typeMode) {
}, TypeMode.DEFAULT ->
autofillSelectionAction = { _, _ -> entryValidatedForSave(entry)
entryValidatedForAutofillSelection(database, entry) TypeMode.MAGIKEYBOARD -> {}
}, TypeMode.PASSKEY ->
autofillRegistrationAction = { entryValidatedForPasskey(database, entry)
entryValidatedForAutofillRegistration(entry) TypeMode.AUTOFILL ->
}, entryValidatedForAutofill(database, entry)
passkeySelectionAction = { }
entryValidatedForPasskeySelection(database, entry)
},
passkeyRegistrationAction = {
entryValidatedForPasskeyRegistration(database, entry)
} }
) )
} }
@@ -487,36 +496,18 @@ class EntryEditActivity : DatabaseLockActivity(),
finishForEntryResult(entry) finishForEntryResult(entry)
} }
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) { private fun entryValidatedForAutofill(database: ContextualDatabase, entry: Entry) {
// Build Autofill response with the entry selected // Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity, this.buildSpecialModeResponseAndSetResult(
database, entryInfo = entry.getEntryInfo(database),
entry.getEntryInfo(database)) extras = buildEntryResult(entry)
}
onValidateSpecialMode()
}
private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
this.buildPasskeyResponseAndSetResult(
entryInfo = entry.getEntryInfo(database)
) )
} }
onValidateSpecialMode() onValidateSpecialMode()
} }
private fun entryValidatedForAutofillRegistration(entry: Entry) { private fun entryValidatedForPasskey(database: ContextualDatabase, entry: Entry) {
//if (isIntentSender()) {
// TODO Autofill Callback #765
//}
onValidateSpecialMode()
if (!isIntentSender()) {
finishForEntryResult(entry)
}
}
private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
this.buildPasskeyResponseAndSetResult( this.buildPasskeyResponseAndSetResult(
entryInfo = entry.getEntryInfo(database), entryInfo = entry.getEntryInfo(database),
@@ -757,13 +748,13 @@ class EntryEditActivity : DatabaseLockActivity(),
} }
private fun onApprovedBackPressed(approved: () -> Unit) { private fun onApprovedBackPressed(approved: () -> Unit) {
if (!backPressedAlreadyApproved) { if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(R.string.discard_changes) .setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ -> .setPositiveButton(R.string.discard) { _, _ ->
mAttachmentFileBinderManager?.stopUploadAllAttachments() mAttachmentFileBinderManager?.stopUploadAllAttachments()
backPressedAlreadyApproved = true mEntryEditViewModel.backPressedAlreadyApproved = true
approved.invoke() approved.invoke()
}.create().show() }.create().show()
} else { } else {
@@ -783,7 +774,7 @@ class EntryEditActivity : DatabaseLockActivity(),
val bundle = buildEntryResult(entry) val bundle = buildEntryResult(entry)
val intentEntry = Intent() val intentEntry = Intent()
intentEntry.putExtras(bundle) intentEntry.putExtras(bundle)
setResult(Activity.RESULT_OK, intentEntry) setResult(RESULT_OK, intentEntry)
super.finish() super.finish()
} catch (e: Exception) { } catch (e: Exception) {
// Exception when parcelable can't be done // Exception when parcelable can't be done
@@ -791,6 +782,10 @@ class EntryEditActivity : DatabaseLockActivity(),
} }
} }
enum class RegistrationType {
UPDATE, CREATE
}
companion object { companion object {
private val TAG = EntryEditActivity::class.java.name private val TAG = EntryEditActivity::class.java.name
@@ -800,23 +795,12 @@ class EntryEditActivity : DatabaseLockActivity(),
const val KEY_PARENT = "parent" const val KEY_PARENT = "parent"
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY" const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
fun registerForEntryResult(fragment: Fragment, fun registerForEntryResult(
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> { activity: FragmentActivity,
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit
if (result.resultCode == Activity.RESULT_OK) { ): ActivityResultLauncher<Intent> {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
}
}
}
fun registerForEntryResult(activity: FragmentActivity,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == RESULT_OK) {
entryAddedOrUpdatedListener.invoke( entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY) result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
) )
@@ -827,176 +811,72 @@ class EntryEditActivity : DatabaseLockActivity(),
} }
/** /**
* Launch EntryEditActivity to update an existing entry by his [entryId] * Launch EntryEditActivity to update an existing entry or to add a new entry in an existing group
*/ */
fun launchToUpdate(activity: Activity, fun launch(
database: ContextualDatabase, activity: Activity,
entryId: NodeId<UUID>, database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>) { registrationType: RegistrationType,
nodeId: NodeId<*>,
activityResultLauncher: ActivityResultLauncher<Intent>
) {
if (database.loaded && !database.isReadOnly) { if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java) val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId) when (registrationType) {
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
}
activityResultLauncher.launch(intent) activityResultLauncher.launch(intent)
} }
} }
} }
/** /**
* Launch EntryEditActivity to add a new entry in an existent group * Launch EntryEditActivity to add a new entry in special selection
*/ */
fun launchToCreate(activity: Activity, fun launchForSelection(
database: ContextualDatabase, context: Context,
groupId: NodeId<*>, database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>) { typeMode: TypeMode,
if (database.loaded && !database.isReadOnly) { groupId: NodeId<*>,
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { searchInfo: SearchInfo? = null,
val intent = Intent(activity, EntryEditActivity::class.java) activityResultLauncher: ActivityResultLauncher<Intent>? = null,
intent.putExtra(KEY_PARENT, groupId) ) {
activityResultLauncher.launch(intent)
}
}
}
fun launchToUpdateForSave(context: Context,
database: ContextualDatabase,
entryId: NodeId<UUID>,
searchInfo: SearchInfo) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
EntrySelectionHelper.startActivityForSaveModeResult(
context,
intent,
searchInfo
)
}
}
}
fun launchToCreateForSave(context: Context,
database: ContextualDatabase,
groupId: NodeId<*>,
searchInfo: SearchInfo) {
if (database.loaded && !database.isReadOnly) { if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java) val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId) intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForSaveModeResult( EntrySelectionHelper.startActivityForSelectionModeResult(
context, context = context,
intent, intent = intent,
searchInfo typeMode = typeMode,
searchInfo = searchInfo,
activityResultLauncher = activityResultLauncher
) )
} }
} }
} }
/** /**
* Launch EntryEditActivity to add a new entry in keyboard selection * Launch EntryEditActivity to update an updated entry or register a new entry (from autofill)
*/ */
fun launchForKeyboardSelectionResult(context: Context, fun launchForRegistration(
database: ContextualDatabase, context: Context,
groupId: NodeId<*>, database: ContextualDatabase,
searchInfo: SearchInfo? = null) { nodeId: NodeId<*>,
registerInfo: RegisterInfo? = null,
typeMode: TypeMode,
registrationType: RegistrationType,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
if (database.loaded && !database.isReadOnly) { if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java) val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId) when (registrationType) {
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult( RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
context, RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
intent, }
searchInfo
)
}
}
}
/**
* Launch EntryEditActivity to add a new entry in autofill selection
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: AppCompatActivity,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
activity,
intent,
activityResultLauncher,
autofillComponent,
searchInfo
)
}
}
}
/**
* Launch EntryEditActivity to add a new passkey entry
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun launchForPasskeySelectionResult(context: Context,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
context,
intent,
activityResultLauncher,
searchInfo
)
}
}
}
/**
* Launch EntryEditActivity to register an updated entry (from autofill)
*/
fun launchToUpdateForRegistration(context: Context,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
entryId: NodeId<UUID>,
registerInfo: RegisterInfo?,
typeMode: TypeMode) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
activityResultLauncher,
intent,
registerInfo,
typeMode
)
}
}
}
/**
* Launch EntryEditActivity to register a new entry (from autofill)
*/
fun launchToCreateForRegistration(context: Context,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
groupId: NodeId<*>,
registerInfo: RegisterInfo? = null,
typeMode: TypeMode) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForRegistrationModeResult( EntrySelectionHelper.startActivityForRegistrationModeResult(
context, context,
activityResultLauncher, activityResultLauncher,

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -33,8 +32,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -50,10 +47,8 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
@@ -99,9 +94,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -131,7 +123,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { uri?.let {
launchPasswordActivityWithPath(uri) launchMainCredentialActivityWithPath(uri)
} }
} }
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri -> mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
@@ -160,7 +152,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen -> mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri -> fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity( launchMainCredentialActivity(
databaseFileUri, databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri, fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey fileDatabaseHistoryEntityToOpen.hardwareKey
@@ -179,7 +171,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Load default database the first time // Load default database the first time
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri -> databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri) launchMainCredentialActivityWithPath(databaseFileUri)
} }
// Retrieve the database URI provided by file manager after an orientation change // Retrieve the database URI provided by file manager after an orientation change
@@ -224,11 +216,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) launchGroupActivityIfLoaded(database)
if (database != null) {
launchGroupActivityIfLoaded(database)
}
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -236,8 +225,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
actionTask: String, actionTask: String,
result: ActionRunnable.Result result: ActionRunnable.Result
) { ) {
super.onDatabaseActionFinished(database, actionTask, result)
if (result.isSuccess) { if (result.isSuccess) {
// Update list // Update list
when (actionTask) { when (actionTask) {
@@ -287,17 +274,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
} }
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) { private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
MainCredentialActivity.launch(this, try {
databaseUri, EntrySelectionHelper.doSpecialAction(
keyFile, intent = this.intent,
hardwareKey, defaultAction = {
{ exception -> MainCredentialActivity.launch(
fileNoFoundAction(exception) activity = this,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey
)
}, },
{ onCancelSpecialMode() }, searchAction = { searchInfo ->
{ onLaunchActivitySpecialMode() }, MainCredentialActivity.launchForSearchResult(
mCredentialActivityResultLauncher) activity = this,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
selectionAction = { intentSenderMode, typeMode, searchInfo ->
MainCredentialActivity.launchForSelection(
activity = this,
activityResultLauncher = if (intentSenderMode)
mCredentialActivityResultLauncher else null,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = typeMode,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
registrationAction = { intentSenderMode, typeMode, registerInfo ->
MainCredentialActivity.launchForRegistration(
activity = this,
activityResultLauncher = if (intentSenderMode)
mCredentialActivityResultLauncher else null,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = typeMode,
registerInfo = registerInfo
)
onLaunchActivitySpecialMode()
}
)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
} }
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) { private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
@@ -307,12 +335,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }, { onLaunchActivitySpecialMode() },
mCredentialActivityResultLauncher) mCredentialActivityResultLauncher
)
} }
} }
private fun launchPasswordActivityWithPath(databaseUri: Uri) { private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null, null) launchMainCredentialActivity(databaseUri, null, null)
// Delete flickering for kitkat <= // Delete flickering for kitkat <=
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
@@ -336,10 +365,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
} }
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
// Show recent files if allowed // Show recent files if allowed
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) { if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
databaseFilesViewModel.loadListOfDatabases() databaseFilesViewModel.loadListOfDatabases()
@@ -358,7 +383,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
try { try {
mDatabaseFileUri?.let { databaseUri -> mDatabaseFileUri?.let { databaseUri ->
// Create the new database // Create the new database
createDatabase(databaseUri, mainCredential) mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
} }
} catch (e: Exception) { } catch (e: Exception) {
val error = getString(R.string.error_create_database_file) val error = getString(R.string.error_create_database_file)
@@ -442,71 +467,35 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* ------------------------- * -------------------------
*/ */
fun launchForSearchResult(context: Context, fun launchForSearchResult(
searchInfo: SearchInfo) { context: Context,
EntrySelectionHelper.startActivityForSearchModeResult(context, searchInfo: SearchInfo
Intent(context, FileDatabaseSelectActivity::class.java), ) {
searchInfo) EntrySelectionHelper.startActivityForSearchModeResult(
context = context,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo = searchInfo
)
} }
/* /*
* ------------------------- * -------------------------
* Save Launch * Selection Launch
* ------------------------- * -------------------------
*/ */
fun launchForSaveResult(context: Context, fun launchForSelection(
searchInfo: SearchInfo) { context: Context,
EntrySelectionHelper.startActivityForSaveModeResult(context, typeMode: TypeMode,
Intent(context, FileDatabaseSelectActivity::class.java), searchInfo: SearchInfo? = null,
searchInfo) activityResultLauncher: ActivityResultLauncher<Intent>? = null,
} ) {
EntrySelectionHelper.startActivityForSelectionModeResult(
/* context = context,
* ------------------------- intent = Intent(context, FileDatabaseSelectActivity::class.java),
* Keyboard Launch searchInfo = searchInfo,
* ------------------------- typeMode = typeMode,
*/ activityResultLauncher = activityResultLauncher
fun launchForKeyboardSelectionResult(activity: Activity,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
searchInfo)
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: AppCompatActivity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
autofillComponent,
searchInfo)
}
/*
* -------------------------
* Passkey Launch
* -------------------------
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun launchForPasskeySelectionResult(activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
searchInfo
) )
} }
@@ -515,16 +504,18 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Registration Launch * Registration Launch
* ------------------------- * -------------------------
*/ */
fun launchForRegistration(context: Context, fun launchForRegistration(
activityResultLauncher: ActivityResultLauncher<Intent>?, context: Context,
registerInfo: RegisterInfo? = null, typeMode: TypeMode,
typeMode: TypeMode) { registerInfo: RegisterInfo? = null,
activityResultLauncher: ActivityResultLauncher<Intent>?,
) {
EntrySelectionHelper.startActivityForRegistrationModeResult( EntrySelectionHelper.startActivityForRegistrationModeResult(
context, context = context,
activityResultLauncher, activityResultLauncher = activityResultLauncher,
Intent(context, FileDatabaseSelectActivity::class.java), intent = Intent(context, FileDatabaseSelectActivity::class.java),
registerInfo, registerInfo = registerInfo,
typeMode typeMode = typeMode
) )
} }
} }

View File

@@ -174,10 +174,10 @@ class IconPickerActivity : DatabaseLockActivity() {
return true return true
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
if (database?.allowCustomIcons == true) { if (database.allowCustomIcons) {
uploadButton.setOpenDocumentClickListener(mExternalFileHelper) uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else { } else {
uploadButton.visibility = View.GONE uploadButton.visibility = View.GONE

View File

@@ -101,7 +101,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
return true return true
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
try { try {
@@ -119,18 +119,16 @@ class ImageViewerActivity : DatabaseLockActivity() {
resources.displayMetrics.heightPixels * 2 resources.displayMetrics.heightPixels * 2
) )
database?.let { database -> BinaryDatabaseManager.loadBitmap(
BinaryDatabaseManager.loadBitmap( database,
database, attachment.binaryData,
attachment.binaryData, mImagePreviewMaxWidth
mImagePreviewMaxWidth ) { bitmapLoaded ->
) { bitmapLoaded -> if (bitmapLoaded == null) {
if (bitmapLoaded == null) { finish()
finish() } else {
} else { progressView.visibility = View.GONE
progressView.visibility = View.GONE imageView.setImageBitmap(bitmapLoaded)
imageView.setImageBitmap(bitmapLoaded)
}
} }
} }
} ?: finish() } ?: finish()

View File

@@ -36,7 +36,6 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
@@ -56,10 +55,8 @@ import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
import com.kunzisoft.keepass.biometric.DeviceUnlockManager import com.kunzisoft.keepass.biometric.DeviceUnlockManager
import com.kunzisoft.keepass.biometric.deviceUnlockError import com.kunzisoft.keepass.biometric.deviceUnlockError
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -128,9 +125,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
private var mReadOnly: Boolean = false private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false private var mForceReadOnly: Boolean = false
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -310,26 +304,20 @@ class MainCredentialActivity : DatabaseModeActivity() {
mDatabaseFileUri?.let { databaseFileUri -> mDatabaseFileUri?.let { databaseFileUri ->
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri) mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
} }
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
if (database != null) { // Trying to load another database
// Trying to load another database if (mDatabaseFileUri != null
if (mDatabaseFileUri != null && database.fileUri != null
&& database.fileUri != null && mDatabaseFileUri != database.fileUri) {
&& mDatabaseFileUri != database.fileUri) { Toast.makeText(this,
Toast.makeText(this, R.string.warning_database_already_opened,
R.string.warning_database_already_opened, Toast.LENGTH_LONG
Toast.LENGTH_LONG ).show()
).show()
}
launchGroupActivityIfLoaded(database)
} }
launchGroupActivityIfLoaded(database)
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -514,10 +502,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
val password = intent.getStringExtra(KEY_PASSWORD) val password = intent.getStringExtra(KEY_PASSWORD)
// Consume the intent extra password // Consume the intent extra password
intent.removeExtra(KEY_PASSWORD) intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) { if (password != null) {
mainCredentialView?.populatePasswordTextView(password) mainCredentialView?.populatePasswordTextView(password)
} }
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
if (launchImmediately) { if (launchImmediately) {
loadDatabase() loadDatabase()
} else { } else {
@@ -572,10 +561,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
clearCredentialsViews() clearCredentialsViews()
} }
if (mReadOnly && ( if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
) {
Log.e(TAG, getString(R.string.error_save_read_only)) Log.e(TAG, getString(R.string.error_save_read_only))
Snackbar.make(coordinatorLayout, Snackbar.make(coordinatorLayout,
R.string.error_save_read_only, R.string.error_save_read_only,
@@ -599,7 +585,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
readOnly: Boolean, readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUUID: Boolean) { fixDuplicateUUID: Boolean) {
loadDatabase( mDatabaseViewModel.loadDatabase(
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, readOnly,
@@ -752,11 +738,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
private const val KEY_PASSWORD = "password" private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity, private fun buildAndLaunchIntent(
databaseFile: Uri, activity: Activity,
keyFile: Uri?, databaseFile: Uri,
hardwareKey: HardwareKey?, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) { hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit
) {
val intent = Intent(activity, MainCredentialActivity::class.java) val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile) intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null) if (keyFile != null)
@@ -773,10 +761,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/ */
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launch(activity: Activity, fun launch(
databaseFile: Uri, activity: Activity,
keyFile: Uri?, databaseFile: Uri,
hardwareKey: HardwareKey?) { keyFile: Uri?,
hardwareKey: HardwareKey?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent) activity.startActivity(intent)
} }
@@ -789,112 +779,56 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/ */
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launchForSearchResult(activity: Activity, fun launchForSearchResult(
databaseFile: Uri, activity: Activity,
keyFile: Uri?, databaseFile: Uri,
hardwareKey: HardwareKey?, keyFile: Uri?,
searchInfo: SearchInfo) { hardwareKey: HardwareKey?,
searchInfo: SearchInfo
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult( EntrySelectionHelper.startActivityForSearchModeResult(
activity, context = activity,
intent, intent = intent,
searchInfo) searchInfo = searchInfo
}
}
/*
* -------------------------
* Save Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForSaveResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSaveModeResult(
activity,
intent,
searchInfo)
}
}
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForKeyboardResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
activity,
intent,
searchInfo)
}
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class)
fun launchForAutofillResult(activity: AppCompatActivity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
activity,
intent,
activityResultLauncher,
autofillComponent,
searchInfo)
}
}
/*
* -------------------------
* Passkey Launch
* -------------------------
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Throws(FileNotFoundException::class)
fun launchForPasskeyResult(activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
activity,
intent,
activityResultLauncher,
searchInfo
) )
} }
} }
/* /*
* ------------------------- * -------------------------
* Registration Launch * Selection Launch
* ------------------------- * -------------------------
*/ */
@Throws(FileNotFoundException::class)
fun launchForSelection(
activity: AppCompatActivity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
typeMode: TypeMode,
searchInfo: SearchInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSelectionModeResult(
context = activity,
intent = intent,
typeMode = typeMode,
searchInfo = searchInfo,
activityResultLauncher = activityResultLauncher
)
}
}
/*
* -------------------------
* Registration Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForRegistration( fun launchForRegistration(
activity: Activity, activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?, activityResultLauncher: ActivityResultLauncher<Intent>?,
@@ -914,121 +848,5 @@ class MainCredentialActivity : DatabaseModeActivity() {
) )
} }
} }
/*
* -------------------------
* Global Launch
* -------------------------
*/
fun launch(activity: AppCompatActivity,
databaseUri: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
activityResultLauncher: ActivityResultLauncher<Intent>?) {
try {
EntrySelectionHelper.doSpecialAction(
intent = activity.intent,
defaultAction = {
launch(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey
)
},
searchAction = { searchInfo ->
launchForSearchResult(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
saveAction = { searchInfo ->
launchForSaveResult(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
keyboardSelectionAction = { searchInfo ->
launchForKeyboardResult(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
autofillSelectionAction = { searchInfo, autofillComponent ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
launchForAutofillResult(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
autofillComponent = autofillComponent,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
autofillRegistrationAction = { registerInfo ->
launchForRegistration(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = TypeMode.AUTOFILL,
registerInfo = registerInfo
)
onLaunchActivitySpecialMode()
},
passkeySelectionAction = { searchInfo ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
launchForPasskeyResult(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
passkeyRegistrationAction = { registerInfo ->
launchForRegistration(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = TypeMode.PASSKEY,
registerInfo = registerInfo
)
onLaunchActivitySpecialMode()
}
)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
}
} }
} }

View File

@@ -67,7 +67,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
} }
builder.setMessage(stringBuilder) builder.setMessage(stringBuilder)
builder.setPositiveButton(android.R.string.ok) { _, _ -> builder.setPositiveButton(android.R.string.ok) { _, _ ->
actionDatabaseListener?.validateDatabaseChanged() actionDatabaseListener?.onDatabaseChangeValidated()
} }
return builder.create() return builder.create()
} }
@@ -76,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
} }
interface ActionDatabaseChangedListener { interface ActionDatabaseChangedListener {
fun validateDatabaseChanged() fun onDatabaseChangeValidated()
} }
companion object { companion object {
@@ -86,9 +86,10 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO" private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE" private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo, fun getInstance(
newSnapFileDatabaseInfo: SnapFileDatabaseInfo, oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
readOnly: Boolean newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
readOnly: Boolean
) )
: DatabaseChangedDialogFragment { : DatabaseChangedDialogFragment {
val fragment = DatabaseChangedDialogFragment() val fragment = DatabaseChangedDialogFragment()

View File

@@ -5,6 +5,9 @@ import android.view.View
import android.view.WindowManager.LayoutParams.FLAG_SECURE import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
@@ -12,23 +15,40 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval { abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: ContextualDatabase? = null private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch {
mDatabaseViewModel.database.observe(this) { database -> repeatOnLifecycle(Lifecycle.State.STARTED) {
this.mDatabase = database mDatabaseViewModel.actionState.collect { uiState ->
resetAppTimeoutOnTouchOrFocus() when (uiState) {
onDatabaseRetrieved(database) is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
}
} }
lifecycleScope.launch {
mDatabaseViewModel.actionFinished.observe(this) { result -> repeatOnLifecycle(Lifecycle.State.RESUMED) {
onDatabaseActionFinished(result.database, result.actionTask, result.result) mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
} }
} }
@@ -52,7 +72,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
resetAppTimeoutOnTouchOrFocus() resetAppTimeoutOnTouchOrFocus()
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Can be overridden by a subclass // Can be overridden by a subclass
} }

View File

@@ -62,14 +62,14 @@ class GroupDialogFragment : DatabaseDialogFragment() {
private lateinit var uuidContainerView: ViewGroup private lateinit var uuidContainerView: ViewGroup
private lateinit var uuidReferenceView: TextView private lateinit var uuidReferenceView: TextView
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon -> mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
} }
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon) mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
if (database?.allowCustomSearchableGroup() == true) { if (database.allowCustomSearchableGroup()) {
searchableLabelView.visibility = View.VISIBLE searchableLabelView.visibility = View.VISIBLE
searchableView.visibility = View.VISIBLE searchableView.visibility = View.VISIBLE
} else { } else {

View File

@@ -112,32 +112,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon -> mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
} }
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon) mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) { searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
View.VISIBLE View.VISIBLE
} else { } else {
View.GONE View.GONE
} }
if (database?.allowAutoType() == true) { if (database.allowAutoType()) {
autoTypeContainerView.visibility = View.VISIBLE autoTypeContainerView.visibility = View.VISIBLE
} else { } else {
autoTypeContainerView.visibility = View.GONE autoTypeContainerView.visibility = View.GONE
} }
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool) tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply { tagsCompletionView.apply {
threshold = 1 threshold = 1
setAdapter(tagsAdapter) setAdapter(tagsAdapter)
} }
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

View File

@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
private var mCustomIcon: IconImageCustom? = null private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon -> mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon) database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
} }
mCustomIcon?.let { customIcon -> mCustomIcon?.let { customIcon ->
populateViewsWithCustomIcon(customIcon) populateViewsWithCustomIcon(customIcon)

View File

@@ -35,9 +35,9 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl import com.kunzisoft.keepass.utils.UriUtil.openUrl
@@ -258,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
showEmptyPasswordConfirmationDialog() showEmptyPasswordConfirmationDialog()
} else if (!error } else if (!error
&& hardwareKey != null && hardwareKey != null
&& !HardwareKeyActivity.isHardwareKeyAvailable( && !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
requireActivity(), hardwareKey, false)
) { ) {
// show hardware driver dialog if required // show hardware driver dialog if required
error = true error = true

View File

@@ -4,36 +4,59 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval { abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected var mDatabase: ContextualDatabase? = null protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database -> override fun onCreate(savedInstanceState: Bundle?) {
if (mDatabase == null || mDatabase != database) { super.onCreate(savedInstanceState)
this.mDatabase = database lifecycleScope.launch {
onDatabaseRetrieved(database) repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
} }
} }
lifecycleScope.launch {
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result -> repeatOnLifecycle(Lifecycle.State.RESUMED) {
onDatabaseActionFinished(result.database, result.actionTask, result.result) mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
} }
} }
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) { protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
context?.let { context?.let {
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded) view?.resetAppTimeoutWhenViewTouchedOrFocused(
context = it,
databaseLoaded = mDatabase?.loaded
)
} }
} }
@@ -44,8 +67,4 @@ abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
) { ) {
// Can be overridden by a subclass // Can be overridden by a subclass
} }
protected fun buildNewBinaryAttachment(): BinaryData? {
return mDatabase?.buildNewBinaryAttachment()
}
} }

View File

@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
val attachmentToUploadUri = it.attachmentToUploadUri val attachmentToUploadUri = it.attachmentToUploadUri
val fileName = it.fileName val fileName = it.fileName
buildNewBinaryAttachment()?.let { binaryAttachment -> mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment) val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment // Ask to replace the current attachment
if ((!mAllowMultipleAttachments if ((!mAllowMultipleAttachments
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
templateView.populateIconMethod = { imageView, icon -> templateView.populateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
} }
mAllowMultipleAttachments = database?.allowMultipleAttachments == true mAllowMultipleAttachments = database.allowMultipleAttachments == true
attachmentsAdapter?.database = database attachmentsAdapter?.database = database
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize -> attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
} }
} }
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool) tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply { tagsCompletionView.apply {
threshold = 1 threshold = 1
setAdapter(tagsAdapter) setAdapter(tagsAdapter)
} }
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
} }
private fun assignEntryInfo(entryInfo: EntryInfo?) { private fun assignEntryInfo(entryInfo: EntryInfo?) {

View File

@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
context?.let { context -> context?.let { context ->
attachmentsAdapter = EntryAttachmentsItemsAdapter(context) attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
attachmentsAdapter?.database = database attachmentsAdapter?.database = database

View File

@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.adapters.NodesAdapter import com.kunzisoft.keepass.adapters.NodesAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.SortNodeEnum
@@ -154,46 +154,44 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onDetach() super.onDetach()
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
context?.let { context -> context?.let { context ->
database?.let { database -> mAdapter = NodesAdapter(context, database).apply {
mAdapter = NodesAdapter(context, database).apply { setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback { override fun onNodeClick(database: ContextualDatabase, node: Node) {
override fun onNodeClick(database: ContextualDatabase, node: Node) { if (nodeActionSelectionMode) {
if (nodeActionSelectionMode) { if (listActionNodes.contains(node)) {
if (listActionNodes.contains(node)) { // Remove selected item if already selected
// Remove selected item if already selected listActionNodes.remove(node)
listActionNodes.remove(node)
} else {
// Add selected item if not already selected
listActionNodes.add(node)
}
nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
} else { } else {
nodeClickListener?.onNodeClick(database, node) // Add selected item if not already selected
listActionNodes.add(node)
} }
nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
} else {
nodeClickListener?.onNodeClick(database, node)
} }
}
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean { override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) { if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click // Select the first item after a long click
if (!listActionNodes.contains(node)) if (!listActionNodes.contains(node))
listActionNodes.add(node) listActionNodes.add(node)
nodeClickListener?.onNodeSelected(database, listActionNodes) nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes) setActionNodes(listActionNodes)
notifyNodeChanged(node) notifyNodeChanged(node)
activity?.hideKeyboard() activity?.hideKeyboard()
}
return true
} }
}) return true
} }
mNodesRecyclerView?.adapter = mAdapter })
} }
mNodesRecyclerView?.adapter = mAdapter
} }
} }
@@ -248,7 +246,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener) mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
activity?.intent?.let { activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it) specialMode = it.retrieveSpecialMode()
} }
} }
@@ -299,9 +297,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} }
} }
private fun containsRecycleBin(nodes: List<Node>): Boolean { private fun containsRecycleBin(database: ContextualDatabase?, nodes: List<Node>): Boolean {
return mDatabase?.isRecycleBinEnabled == true return database?.isRecycleBinEnabled == true
&& nodes.any { it == mDatabase?.recycleBin } && nodes.any { it == database.recycleBin }
} }
fun actionNodesCallback(database: ContextualDatabase, fun actionNodesCallback(database: ContextualDatabase,
@@ -328,7 +326,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
// Open and Edit for a single item // Open and Edit for a single item
if (nodes.size == 1) { if (nodes.size == 1) {
// Edition // Edition
if (database.isReadOnly || containsRecycleBin(nodes)) { if (database.isReadOnly || containsRecycleBin(database, nodes)) {
menu?.removeItem(R.id.menu_edit) menu?.removeItem(R.id.menu_edit)
} }
} else { } else {
@@ -348,7 +346,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} }
// Deletion // Deletion
if (database.isReadOnly || containsRecycleBin(nodes)) { if (database.isReadOnly || containsRecycleBin(database, nodes)) {
menu?.removeItem(R.id.menu_delete) menu?.removeItem(R.id.menu_delete)
} }
} }

View File

@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
resetAppTimeoutWhenViewFocusedOrChanged(view) resetAppTimeoutWhenViewFocusedOrChanged(view)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val populateList = launch { val populateList = launch {

View File

@@ -48,9 +48,9 @@ class IconPickerFragment : DatabaseFragment() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerPagerAdapter = IconPickerPagerAdapter(this, iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (database?.allowCustomIcons == true) 2 else 1) if (database.allowCustomIcons) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position -> TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) { tab.text = when (position) {

View File

@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
super.onDestroyView() super.onDestroyView()
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here // Nothing here
} }

View File

@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here // Nothing here
} }

View File

@@ -293,20 +293,22 @@ class PasswordGeneratorFragment : DatabaseFragment() {
private fun generatePassword() { private fun generatePassword() {
var password = "" var password = ""
try { try {
password = PasswordGenerator(resources).generatePassword(getPasswordLength(), password = PasswordGenerator(resources).generatePassword(
uppercaseCompound.isChecked, length = getPasswordLength(),
lowercaseCompound.isChecked, upperCase = uppercaseCompound.isChecked,
digitsCompound.isChecked, lowerCase = lowercaseCompound.isChecked,
minusCompound.isChecked, digits = digitsCompound.isChecked,
underlineCompound.isChecked, minus = minusCompound.isChecked,
spaceCompound.isChecked, underline = underlineCompound.isChecked,
specialsCompound.isChecked, space = spaceCompound.isChecked,
bracketsCompound.isChecked, specials = specialsCompound.isChecked,
extendedCompound.isChecked, brackets = bracketsCompound.isChecked,
getConsiderChars(), extended = extendedCompound.isChecked,
getIgnoreChars(), considerChars = getConsiderChars(),
atLeastOneCompound.isChecked, ignoreChars = getIgnoreChars(),
excludeAmbiguousCompound.isChecked) atLeastOneFromEach = atLeastOneCompound.isChecked,
excludeAmbiguousChar = excludeAmbiguousCompound.isChecked
)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e) Log.e(TAG, "Unable to generate a password", e)
} }
@@ -318,7 +320,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
super.onDestroy() super.onDestroy()
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here // Nothing here
} }

View File

@@ -1,104 +1,240 @@
package com.kunzisoft.keepass.activities.legacy package com.kunzisoft.keepass.activities.legacy
import android.net.Uri import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.ProgressMessage
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getBinaryDir import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval { abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels() protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null protected val mDatabase: ContextualDatabase?
protected var mDatabase: ContextualDatabase? = null get() = mDatabaseViewModel.database
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
private val mActionDatabaseListener =
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
override fun onDatabaseChangeValidated() {
mDatabaseViewModel.onDatabaseChangeValidated()
}
}
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { _ ->
// Whether or not the user has accepted, the service can be started,
// There just won't be any notification if it's not allowed.
tempServiceParameters.removeFirstOrNull()?.let {
startDatabaseService(it.first, it.second)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.Loading -> {}
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
if (finishActivityIfReloadRequested()) {
finish()
}
}
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog()) is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> {
if (manageDatabaseInfo()) {
showDatabaseChangedDialog(
uiState.previousDatabaseInfo,
uiState.newDatabaseInfo,
uiState.readOnlyDatabase
)
}
}
mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
val databaseWasReloaded = database?.wasReloaded == true startDatabasePermissionService(
if (databaseWasReloaded && finishActivityIfReloadRequested()) { uiState.bundle,
finish() uiState.actionTask
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) { )
database?.wasReloaded = false }
onDatabaseRetrieved(database)
is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
if (showDatabaseDialog())
startDialog(uiState.progressMessage)
}
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
if (showDatabaseDialog())
updateDialog(uiState.progressMessage)
}
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
// Remove the progress task
stopDialog()
}
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
stopDialog()
}
}
}
} }
} }
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result -> lifecycleScope.launch {
onDatabaseActionFinished(database, actionTask, result) repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
// Nullable function
onUnknownDatabaseRetrieved(database)
database?.let {
onDatabaseRetrieved(database)
}
}
}
} }
} }
protected open fun showDatabaseDialog(): Boolean { /**
return true * Nullable function to retrieve a database
} */
open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
mDatabase = null
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
// optional method implementation // optional method implementation
} }
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// optional method implementation
}
open fun manageDatabaseInfo(): Boolean = true
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
database: ContextualDatabase, database: ContextualDatabase,
actionTask: String, actionTask: String,
result: ActionRunnable.Result result: ActionRunnable.Result
) { ) {
mDatabaseViewModel.onActionFinished(database, actionTask, result)
// optional method implementation // optional method implementation
} }
fun createDatabase( private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
databaseUri: Uri, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mainCredential: MainCredential if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED
) {
startDatabaseService(bundle, actionTask)
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.POST_NOTIFICATIONS
)
) {
// it's not the first time, so the user deliberately chooses not to display the notification
startDatabaseService(bundle, actionTask)
} else {
AlertDialog.Builder(this)
.setMessage(R.string.warning_database_notification_permission)
.setNegativeButton(R.string.later) { _, _ ->
// Refuses the notification, so start the service
startDatabaseService(bundle, actionTask)
}
.setPositiveButton(R.string.ask) { _, _ ->
// Save the temp parameters to ask the permission
tempServiceParameters.add(Pair(bundle, actionTask))
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}.create().show()
}
} else {
startDatabaseService(bundle, actionTask)
}
}
private fun showDatabaseChangedDialog(
previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo,
readOnlyDatabase: Boolean
) { ) {
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential) lifecycleScope.launch {
if (databaseChangedDialogFragment == null) {
databaseChangedDialogFragment = supportFragmentManager
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
}
if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
previousDatabaseInfo,
newDatabaseInfo,
readOnlyDatabase
)
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
databaseChangedDialogFragment?.show(
supportFragmentManager,
DATABASE_CHANGED_DIALOG_TAG
)
}
}
} }
fun loadDatabase( private fun startDialog(progressMessage: ProgressMessage) {
databaseUri: Uri, lifecycleScope.launch {
mainCredential: MainCredential, if (progressTaskDialogFragment == null) {
readOnly: Boolean, progressTaskDialogFragment = supportFragmentManager
cipherEncryptDatabase: CipherEncryptDatabase?, .findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
fixDuplicateUuid: Boolean }
) { if (progressTaskDialogFragment == null) {
mDatabaseTaskProvider?.startDatabaseLoad( progressTaskDialogFragment = ProgressTaskDialogFragment()
databaseUri, progressTaskDialogFragment?.show(
mainCredential, supportFragmentManager,
readOnly, PROGRESS_TASK_DIALOG_TAG
cipherEncryptDatabase, )
fixDuplicateUuid }
) updateDialog(progressMessage)
}
} }
protected fun closeDatabase() { private fun updateDialog(progressMessage: ProgressMessage) {
mDatabase?.clearAndClose(this.getBinaryDir()) progressTaskDialogFragment?.apply {
updateTitle(progressMessage.titleId)
updateMessage(progressMessage.messageId)
updateWarning(progressMessage.warningId)
setCancellable(progressMessage.cancelable)
}
} }
override fun onResume() { private fun stopDialog() {
super.onResume() progressTaskDialogFragment?.dismissAllowingStateLoss()
mDatabaseTaskProvider?.registerProgressTask() progressTaskDialogFragment = null
} }
override fun onPause() { protected open fun showDatabaseDialog(): Boolean {
mDatabaseTaskProvider?.unregisterProgressTask() return true
super.onPause()
} }
} }

View File

@@ -34,7 +34,7 @@ import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
@@ -87,128 +87,44 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
deleteDatabaseNodes(nodes) deleteDatabaseNodes(nodes)
} }
mDatabaseViewModel.saveDatabase.observe(this) { save ->
mDatabaseTaskProvider?.startDatabaseSave(save)
}
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
mDatabaseTaskProvider?.startDatabaseMerge(save)
}
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
}
}
mDatabaseViewModel.saveName.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveDescription.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveDefaultUsername.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveColor.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveCompression.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.removeUnlinkData.observe(this) {
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
}
mDatabaseViewModel.saveRecycleBin.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveEncryption.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveKeyDerivation.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveIterations.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMemoryUsage.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveParallelism.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
}
mExitLock = false mExitLock = false
} }
open fun finishActivityIfDatabaseNotLoaded(): Boolean { override fun onDatabaseRetrieved(database: ContextualDatabase) {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
// End activity if database not loaded // End activity if database not loaded
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) { if (database.loaded.not())
finish() finish()
}
// Focus view to reinitialize timeout, // Focus view to reinitialize timeout,
// view is not necessary loaded so retry later in resume // view is not necessary loaded so retry later in resume
viewToInvalidateTimeout() viewToInvalidateTimeout()
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded) ?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
database?.let { // check timeout
// check timeout if (mTimeoutEnable) {
if (mTimeoutEnable) { if (mLockReceiver == null) {
if (mLockReceiver == null) { mLockReceiver = LockReceiver {
mLockReceiver = LockReceiver { closeDatabase(database)
mDatabase = null mExitLock = true
closeDatabase(database) closeOptionsMenu()
mExitLock = true finish()
closeOptionsMenu()
finish()
}
registerLockReceiver(mLockReceiver)
} }
registerLockReceiver(mLockReceiver)
// After the first creation
// or If simply swipe with another application
// If the time is out -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
// If onCreate already record time
if (!mExitLock)
TimeoutHelper.recordTime(this, database.loaded)
} }
mDatabaseReadOnly = database.isReadOnly // After the first creation
mMergeDataAllowed = database.isMergeDataAllowed() // or If simply swipe with another application
// If the time is out -> close the Activity
checkRegister() TimeoutHelper.checkTimeAndLockIfTimeout(this)
// If onCreate already record time
if (!mExitLock)
TimeoutHelper.recordTime(this, database.loaded)
} }
mDatabaseReadOnly = database.isReadOnly
mMergeDataAllowed = database.isMergeDataAllowed()
checkRegister()
} }
override fun finish() { override fun finish() {
@@ -227,7 +143,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
actionTask: String, actionTask: String,
result: ActionRunnable.Result result: ActionRunnable.Result
) { ) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) { when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK, DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
@@ -249,24 +164,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
databaseUri: Uri?, databaseUri: Uri?,
mainCredential: MainCredential mainCredential: MainCredential
) { ) {
assignDatabasePassword(databaseUri, mainCredential) mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
} }
private fun assignDatabasePassword( fun assignMainCredential(mainCredential: MainCredential) {
databaseUri: Uri?,
mainCredential: MainCredential
) {
if (databaseUri != null) {
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
}
}
fun assignPassword(mainCredential: MainCredential) {
mDatabase?.let { database -> mDatabase?.let { database ->
database.fileUri?.let { databaseUri -> database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation // Show the progress dialog now or after dialog confirmation
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) { if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
assignDatabasePassword(databaseUri, mainCredential) mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
} else { } else {
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential) PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
.show(supportFragmentManager, "passwordEncodingTag") .show(supportFragmentManager, "passwordEncodingTag")
@@ -276,45 +182,51 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
fun saveDatabase() { fun saveDatabase() {
mDatabaseTaskProvider?.startDatabaseSave(true) mDatabaseViewModel.saveDatabase(save = true)
} }
fun saveDatabaseTo(uri: Uri) { fun saveDatabaseTo(uri: Uri) {
mDatabaseTaskProvider?.startDatabaseSave(true, uri) mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
} }
fun mergeDatabase() { fun mergeDatabase() {
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable) mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
} }
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) { fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential) mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
} }
fun reloadDatabase() { fun reloadDatabase() {
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) { mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
mDatabaseTaskProvider?.startDatabaseReload(false)
}
} }
fun createEntry(newEntry: Entry, fun createEntry(
parent: Group) { newEntry: Entry,
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable) parent: Group
) {
mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
} }
fun updateEntry(oldEntry: Entry, fun updateEntry(
entryToUpdate: Entry) { oldEntry: Entry,
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable) entryToUpdate: Entry
) {
mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
} }
fun copyNodes(nodesToCopy: List<Node>, fun copyNodes(
newParent: Group) { nodesToCopy: List<Node>,
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable) newParent: Group
) {
mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
} }
fun moveNodes(nodesToMove: List<Node>, fun moveNodes(
newParent: Group) { nodesToMove: List<Node>,
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable) newParent: Group
) {
mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
} }
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean { private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
@@ -330,6 +242,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) { fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
// TODO Move in ViewModel
mDatabase?.let { database -> mDatabase?.let { database ->
// If recycle bin enabled, ensure it exists // If recycle bin enabled, ensure it exists
if (database.isRecycleBinEnabled) { if (database.isRecycleBinEnabled) {
@@ -350,11 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
private fun deleteDatabaseNodes(nodes: List<Node>) { private fun deleteDatabaseNodes(nodes: List<Node>) {
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable) mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
} }
fun createGroup(parent: Group, fun createGroup(
groupInfo: GroupInfo?) { parent: Group,
groupInfo: GroupInfo?
) {
// TODO Move in ViewModel
// Build the group // Build the group
mDatabase?.createGroup()?.let { newGroup -> mDatabase?.createGroup()?.let { newGroup ->
groupInfo?.let { info -> groupInfo?.let { info ->
@@ -362,12 +278,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
// Not really needed here because added in runnable but safe // Not really needed here because added in runnable but safe
newGroup.parent = parent newGroup.parent = parent
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable) mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
} }
} }
fun updateGroup(oldGroup: Group, fun updateGroup(
groupInfo: GroupInfo) { oldGroup: Group,
groupInfo: GroupInfo
) {
// TODO Move in ViewModel
// If group updated save it in the database // If group updated save it in the database
val updateGroup = Group(oldGroup).let { updateGroup -> val updateGroup = Group(oldGroup).let { updateGroup ->
updateGroup.apply { updateGroup.apply {
@@ -377,27 +296,28 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
this.setGroupInfo(groupInfo) this.setGroupInfo(groupInfo)
} }
} }
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable) mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
} }
fun restoreEntryHistory(mainEntryId: NodeId<UUID>, fun restoreEntryHistory(
entryHistoryPosition: Int) { mainEntryId: NodeId<UUID>,
mDatabaseTaskProvider entryHistoryPosition: Int
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable) ) {
mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
} }
fun deleteEntryHistory(mainEntryId: NodeId<UUID>, fun deleteEntryHistory(
entryHistoryPosition: Int) { mainEntryId: NodeId<UUID>,
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable) entryHistoryPosition: Int
) {
mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
} }
private fun checkRegister() { private fun checkRegister() {
// If in ave or registration mode, don't allow read only // If in registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
|| mSpecialMode == SpecialMode.REGISTRATION)
&& mDatabaseReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
finish() finish()
} }
} }

View File

@@ -1,12 +1,21 @@
package com.kunzisoft.keepass.activities.legacy package com.kunzisoft.keepass.activities.legacy
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
@@ -21,10 +30,25 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
abstract class DatabaseModeActivity : DatabaseActivity() { abstract class DatabaseModeActivity : DatabaseActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
private var mTypeMode: TypeMode = TypeMode.DEFAULT protected var mTypeMode: TypeMode = TypeMode.DEFAULT
private var mToolbarSpecial: ToolbarSpecial? = null private var mToolbarSpecial: ToolbarSpecial? = null
/**
* Utility activity result launcher,
* Used recursively, close each activity with return data
*/
protected open var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
setActivityResult(
lockDatabase = false,
resultCode = it.resultCode,
data = it.data
)
}
open fun onDatabaseBackPressed() { open fun onDatabaseBackPressed() {
if (mSpecialMode != SpecialMode.DEFAULT) if (mSpecialMode != SpecialMode.DEFAULT)
onCancelSpecialMode() onCancelSpecialMode()
@@ -50,8 +74,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
fun onLaunchActivitySpecialMode() { fun onLaunchActivitySpecialMode() {
if (!isIntentSender()) { if (!isIntentSender()) {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
finish() finish()
} }
} }
@@ -60,8 +84,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
if (isIntentSender()) { if (isIntentSender()) {
super.finish() super.finish()
} else { } else {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish() backToTheMainAppAndFinish()
} }
@@ -73,8 +97,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
// To get the app caller, only for IntentSender // To get the app caller, only for IntentSender
onRegularBackPressed() onRegularBackPressed()
} else { } else {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish() backToTheMainAppAndFinish()
} }
@@ -105,18 +129,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
} }
}) })
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent) mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent) mTypeMode = intent.retrieveTypeMode()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent) mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent) mTypeMode = intent.retrieveTypeMode()
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent) val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
val searchInfo: SearchInfo? = registerInfo?.searchInfo val searchInfo: SearchInfo? = registerInfo?.searchInfo
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) ?: intent.retrieveSearchInfo()
// To show the selection mode // To show the selection mode
mToolbarSpecial = findViewById(R.id.special_mode_view) mToolbarSpecial = findViewById(R.id.special_mode_view)
@@ -125,9 +149,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
val selectionModeStringId = when (mSpecialMode) { val selectionModeStringId = when (mSpecialMode) {
SpecialMode.DEFAULT, // Not important because hidden SpecialMode.DEFAULT, // Not important because hidden
SpecialMode.SEARCH -> R.string.search_mode SpecialMode.SEARCH -> R.string.search_mode
SpecialMode.SAVE -> R.string.save_mode
SpecialMode.SELECTION -> R.string.selection_mode SpecialMode.SELECTION -> R.string.selection_mode
SpecialMode.REGISTRATION -> R.string.registration_mode SpecialMode.REGISTRATION -> R.string.save_mode // Save is registration mode
} }
val typeModeStringId = when (mTypeMode) { val typeModeStringId = when (mTypeMode) {
TypeMode.DEFAULT, // Not important because hidden TypeMode.DEFAULT, // Not important because hidden
@@ -145,7 +168,6 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
visible = when (mSpecialMode) { visible = when (mSpecialMode) {
SpecialMode.DEFAULT -> false SpecialMode.DEFAULT -> false
SpecialMode.SEARCH -> true SpecialMode.SEARCH -> true
SpecialMode.SAVE -> true
SpecialMode.SELECTION -> true SpecialMode.SELECTION -> true
SpecialMode.REGISTRATION -> true SpecialMode.REGISTRATION -> true
} }

View File

@@ -4,8 +4,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
interface DatabaseRetrieval { interface DatabaseRetrieval {
fun onDatabaseRetrieved(database: ContextualDatabase?) fun onDatabaseRetrieved(database: ContextualDatabase)
fun onDatabaseActionFinished(database: ContextualDatabase,
actionTask: String, fun onDatabaseActionFinished(
result: ActionRunnable.Result) database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
)
} }

View File

@@ -24,26 +24,26 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.util.Log import android.util.Log
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.getEnumExtra import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.getParcelableList
import com.kunzisoft.keepass.utils.putEnumExtra import com.kunzisoft.keepass.utils.putEnumExtra
import com.kunzisoft.keepass.utils.putParcelableList
import java.util.UUID
object EntrySelectionHelper { object EntrySelectionHelper {
@@ -51,6 +51,8 @@ object EntrySelectionHelper {
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE" private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO" private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO" private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
private const val EXTRA_NODES_IDS = "com.kunzisoft.keepass.extra.NODES_IDS"
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.NODE_ID"
/** /**
* Finish the activity by passing the result code and by locking the database if necessary * Finish the activity by passing the result code and by locking the database if necessary
@@ -58,7 +60,7 @@ object EntrySelectionHelper {
fun Activity.setActivityResult( fun Activity.setActivityResult(
lockDatabase: Boolean = false, lockDatabase: Boolean = false,
resultCode: Int, resultCode: Int,
data: Intent? = null, data: Intent? = null
) { ) {
when (resultCode) { when (resultCode) {
Activity.RESULT_OK -> Activity.RESULT_OK ->
@@ -68,170 +70,167 @@ object EntrySelectionHelper {
} }
this.finish() this.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) { if (lockDatabase) {
// Close the database // Close the database
this.sendBroadcast(Intent(LOCK_ACTION)) this.sendBroadcast(Intent(LOCK_ACTION))
} }
} }
/** fun startActivityForSearchModeResult(
* Utility method to build a registerForActivityResult, context: Context,
* Used recursively, close each activity with return data intent: Intent,
*/ searchInfo: SearchInfo
fun AppCompatActivity.buildActivityResultLauncher( ) {
lockDatabase: Boolean = false, intent.addSpecialMode(SpecialMode.SEARCH)
dataTransformation: (data: Intent?) -> Intent? = { it }, intent.addSearchInfo(searchInfo)
): ActivityResultLauncher<Intent> { intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
return this.registerForActivityResult( context.startActivity(intent)
ActivityResultContracts.StartActivityForResult() }
) {
setActivityResult( fun startActivityForSelectionModeResult(
lockDatabase, context: Context,
it.resultCode, intent: Intent,
dataTransformation(it.data) typeMode: TypeMode,
) searchInfo: SearchInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
intent.addSpecialMode(SpecialMode.SELECTION)
intent.addTypeMode(typeMode)
intent.addSearchInfo(searchInfo)
if (activityResultLauncher == null) {
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
} activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
fun startActivityForSearchModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo) {
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun startActivityForSaveModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo) {
addSpecialModeInIntent(intent, SpecialMode.SAVE)
addTypeModeInIntent(intent, TypeMode.DEFAULT)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun startActivityForKeyboardSelectionModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo?) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
/**
* Utility method to start an activity with an Autofill for result
*/
@RequiresApi(Build.VERSION_CODES.O)
fun startActivityForAutofillSelectionModeResult(
context: Context,
intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?
) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
intent.addAutofillComponent(context, autofillComponent)
addSearchInfoInIntent(intent, searchInfo)
activityResultLauncher?.launch(intent)
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun startActivityForPasskeySelectionModeResult(
context: Context,
intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
searchInfo: SearchInfo?
) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
addTypeModeInIntent(intent, TypeMode.PASSKEY)
addSearchInfoInIntent(intent, searchInfo)
activityResultLauncher?.launch(intent)
} }
fun startActivityForRegistrationModeResult( fun startActivityForRegistrationModeResult(
context: Context?, context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>?, activityResultLauncher: ActivityResultLauncher<Intent>?,
intent: Intent, intent: Intent,
registerInfo: RegisterInfo?, registerInfo: RegisterInfo?,
typeMode: TypeMode typeMode: TypeMode
) { ) {
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION) intent.addSpecialMode(SpecialMode.REGISTRATION)
addTypeModeInIntent(intent, typeMode) intent.addTypeMode(typeMode)
addRegisterInfoInIntent(intent, registerInfo) intent.addRegisterInfo(registerInfo)
if (activityResultLauncher == null) { if (activityResultLauncher == null) {
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
activityResultLauncher?.launch(intent) ?: context?.startActivity(intent) ?: activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
throw IllegalStateException("At least Context or ActivityResultLauncher must not be null")
} }
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) { /**
* Build the special mode response for internal entry selection for one entry
*/
fun Activity.buildSpecialModeResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
this.buildSpecialModeResponseAndSetResult(listOf(entryInfo), extras)
}
/**
* Build the special mode response for internal entry selection for multiple entries
*/
fun Activity.buildSpecialModeResponseAndSetResult(
entriesInfo: List<EntryInfo>,
extras: Bundle? = null
) {
try {
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success special mode manual selection")
mReplyIntent.addNodesIds(entriesInfo.map { it.id })
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the result", e)
setResult(Activity.RESULT_CANCELED)
}
}
fun Intent.addSearchInfo(searchInfo: SearchInfo?): Intent {
searchInfo?.let { searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it) putExtra(KEY_SEARCH_INFO, it)
} }
return this
} }
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? { fun Intent.retrieveSearchInfo(): SearchInfo? {
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO) return getParcelableExtraCompat(KEY_SEARCH_INFO)
} }
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) { fun Intent.addRegisterInfo(registerInfo: RegisterInfo?): Intent {
registerInfo?.let { registerInfo?.let {
intent.putExtra(KEY_REGISTER_INFO, it) putExtra(KEY_REGISTER_INFO, it)
} }
return this
} }
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? { fun Intent.retrieveRegisterInfo(): RegisterInfo? {
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO) return getParcelableExtraCompat(KEY_REGISTER_INFO)
} }
fun removeInfoFromIntent(intent: Intent) { fun Intent.removeInfo() {
intent.removeExtra(KEY_SEARCH_INFO) removeExtra(KEY_SEARCH_INFO)
intent.removeExtra(KEY_REGISTER_INFO) removeExtra(KEY_REGISTER_INFO)
} }
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
// TODO Replace by Intent.addSpecialMode
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
}
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent { fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode) this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
return this return this
} }
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode { fun Intent.retrieveSpecialMode(): SpecialMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return SpecialMode.SELECTION
}
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
} }
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
// TODO Replace by Intent.addTypeMode
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
}
fun Intent.addTypeMode(typeMode: TypeMode): Intent { fun Intent.addTypeMode(typeMode: TypeMode): Intent {
this.putEnumExtra(KEY_TYPE_MODE, typeMode) this.putEnumExtra(KEY_TYPE_MODE, typeMode)
return this return this
} }
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode { fun Intent.retrieveTypeMode(): TypeMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return TypeMode.AUTOFILL
}
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
} }
fun removeModesFromIntent(intent: Intent) { fun Intent.removeModes() {
intent.removeExtra(KEY_SPECIAL_MODE) removeExtra(KEY_SPECIAL_MODE)
intent.removeExtra(KEY_TYPE_MODE) removeExtra(KEY_TYPE_MODE)
}
fun Intent.addNodesIds(nodesIds: List<UUID>): Intent {
this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
return this
}
fun Intent.retrieveNodesIds(): List<UUID>? {
return getParcelableList<ParcelUuid>(EXTRA_NODES_IDS)?.map { it.uuid }
}
fun Intent.removeNodesIds() {
removeExtra(EXTRA_NODES_IDS)
}
/**
* Add the node id to the intent
*/
fun Intent.addNodeId(nodeId: UUID?) {
nodeId?.let {
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
}
}
/**
* Retrieve the node id from the intent
*/
fun Intent.retrieveNodeId(): UUID? {
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
}
fun Intent.removeNodeId() {
removeExtra(EXTRA_NODE_ID)
} }
/** /**
@@ -240,102 +239,100 @@ object EntrySelectionHelper {
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean { fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
return (specialMode == SpecialMode.SELECTION return (specialMode == SpecialMode.SELECTION
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY)) && (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|| (specialMode == SpecialMode.REGISTRATION || (specialMode == SpecialMode.REGISTRATION
&& typeMode == TypeMode.PASSKEY) && (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
} }
fun doSpecialAction(intent: Intent, fun doSpecialAction(
defaultAction: () -> Unit, intent: Intent,
searchAction: (searchInfo: SearchInfo) -> Unit, defaultAction: () -> Unit,
saveAction: (searchInfo: SearchInfo) -> Unit, searchAction: (searchInfo: SearchInfo) -> Unit,
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit, selectionAction: (
autofillSelectionAction: (searchInfo: SearchInfo?, intentSenderMode: Boolean,
autofillComponent: AutofillComponent) -> Unit, typeMode: TypeMode,
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit, searchInfo: SearchInfo?
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit, ) -> Unit,
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) { registrationAction: (
intentSenderMode: Boolean,
when (retrieveSpecialModeFromIntent(intent)) { typeMode: TypeMode,
registerInfo: RegisterInfo?
) -> Unit
) {
when (val specialMode = intent.retrieveSpecialMode()) {
SpecialMode.DEFAULT -> { SpecialMode.DEFAULT -> {
removeModesFromIntent(intent) intent.removeModes()
removeInfoFromIntent(intent) intent.removeInfo()
defaultAction.invoke() defaultAction.invoke()
} }
SpecialMode.SEARCH -> { SpecialMode.SEARCH -> {
val searchInfo = retrieveSearchInfoFromIntent(intent) val searchInfo = intent.retrieveSearchInfo()
removeModesFromIntent(intent) intent.removeModes()
removeInfoFromIntent(intent) intent.removeInfo()
if (searchInfo != null) if (searchInfo != null)
searchAction.invoke(searchInfo) searchAction.invoke(searchInfo)
else { else {
defaultAction.invoke() defaultAction.invoke()
} }
} }
SpecialMode.SAVE -> {
val searchInfo = retrieveSearchInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
if (searchInfo != null)
saveAction.invoke(searchInfo)
else {
defaultAction.invoke()
}
}
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent) val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
var autofillComponentInit = false if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { when (val typeMode = intent.retrieveTypeMode()) {
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent -> TypeMode.DEFAULT -> {
autofillSelectionAction.invoke(searchInfo, autofillComponent) intent.removeModes()
autofillComponentInit = true if (searchInfo != null)
} searchAction.invoke(searchInfo)
} else
if (!autofillComponentInit) { defaultAction.invoke()
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) { }
when (retrieveTypeModeFromIntent(intent)) { TypeMode.MAGIKEYBOARD -> selectionAction.invoke(
TypeMode.DEFAULT -> { isIntentSenderMode(specialMode, typeMode),
removeModesFromIntent(intent) typeMode,
if (searchInfo != null) searchInfo
searchAction.invoke(searchInfo) )
else TypeMode.PASSKEY ->
defaultAction.invoke() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
} selectionAction.invoke(
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo) isIntentSenderMode(specialMode, typeMode),
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo) typeMode,
else -> { searchInfo
// In this case, error )
removeModesFromIntent(intent) } else
removeInfoFromIntent(intent) 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 -> { SpecialMode.REGISTRATION -> {
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent) val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
if (!isIntentSenderMode( val typeMode = intent.retrieveTypeMode()
specialMode = retrieveSpecialModeFromIntent(intent), val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
typeMode = retrieveTypeModeFromIntent(intent)) if (!intentSenderMode) {
) { intent.removeModes()
removeModesFromIntent(intent) intent.removeInfo()
removeInfoFromIntent(intent)
} }
when (retrieveTypeModeFromIntent(intent)) { if (registerInfo != null)
TypeMode.AUTOFILL -> { registrationAction.invoke(
autofillRegistrationAction.invoke(registerInfo) intentSenderMode,
} typeMode,
TypeMode.PASSKEY -> { registerInfo
passkeyRegistrationAction.invoke(registerInfo) )
} else {
else -> { defaultAction.invoke()
// Do other registration type
}
} }
} }
} }
@@ -367,7 +364,7 @@ object EntrySelectionHelper {
try { try {
database.iconDrawableFactory.getBitmapFromIcon(context, database.iconDrawableFactory.getBitmapFromIcon(context,
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap -> this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
return Icon.createWithBitmap(bitmap) return IconCompat.createWithBitmap(bitmap).toIcon(context)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e) Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)

View File

@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.credentialprovider
enum class SpecialMode { enum class SpecialMode {
DEFAULT, DEFAULT,
SEARCH, SEARCH,
SAVE,
SELECTION, SELECTION,
REGISTRATION; REGISTRATION;
} }

View File

@@ -1,5 +1,5 @@
package com.kunzisoft.keepass.credentialprovider package com.kunzisoft.keepass.credentialprovider
enum class TypeMode { enum class TypeMode {
DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
} }

View File

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

View File

@@ -22,19 +22,17 @@ package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.core.net.toUri import androidx.core.net.toUri
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.view.toastError
@@ -53,8 +51,8 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
return false return false
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database) super.onUnknownDatabaseRetrieved(database)
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE) val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
if (keySelectionBundle != null) { if (keySelectionBundle != null) {
@@ -88,7 +86,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
if (OtpEntryFields.isOTPUri(extra)) if (OtpEntryFields.isOTPUri(extra))
otpString = extra otpString = extra
} }
launchSelection(database, sharedWebDomain, otpString) launchSelection(database, null, otpString)
} }
else -> { else -> {
if (database != null) { if (database != null) {
@@ -110,11 +108,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
this.webDomain = sharedWebDomain this.webDomain = sharedWebDomain
this.otpString = otpString this.otpString = otpString
} }
launch(database, searchInfo)
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
}
} }
private fun launch(database: ContextualDatabase?, private fun launch(database: ContextualDatabase?,
@@ -133,11 +127,12 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
// Items found // Items found
if (searchInfo.otpString != null) { if (searchInfo.otpString != null) {
if (!readOnly) { if (!readOnly) {
GroupActivity.launchForSaveResult( GroupActivity.launchForRegistration(
this, context = this,
openedDatabase, activityResultLauncher = null,
searchInfo, database = openedDatabase,
false registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
) )
} else { } else {
toastError(RegisterInReadOnlyDatabaseException()) toastError(RegisterInReadOnlyDatabaseException())
@@ -153,11 +148,12 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
) )
}, },
{ autoSearch -> { autoSearch ->
GroupActivity.launchForKeyboardSelectionResult( GroupActivity.launchForSelection(
this, context = this,
openedDatabase, database = openedDatabase,
searchInfo, typeMode = TypeMode.MAGIKEYBOARD,
autoSearch searchInfo = searchInfo,
autoSearch = autoSearch
) )
} }
) )
@@ -174,21 +170,23 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
// Show the database UI to select the entry // Show the database UI to select the entry
if (searchInfo.otpString != null) { if (searchInfo.otpString != null) {
if (!readOnly) { if (!readOnly) {
GroupActivity.launchForSaveResult( GroupActivity.launchForRegistration(
this, context = this,
openedDatabase, activityResultLauncher = null,
searchInfo, database = openedDatabase,
false registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
) )
} else { } else {
toastError(RegisterInReadOnlyDatabaseException()) toastError(RegisterInReadOnlyDatabaseException())
} }
} else if (searchShareForMagikeyboard) { } else if (searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult( GroupActivity.launchForSelection(
this, context = this,
openedDatabase, database = openedDatabase,
searchInfo, typeMode = TypeMode.MAGIKEYBOARD,
false searchInfo = searchInfo,
autoSearch = false
) )
} else { } else {
GroupActivity.launchForSearchResult( GroupActivity.launchForSearchResult(
@@ -202,14 +200,17 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
onDatabaseClosed = { onDatabaseClosed = {
// If database not open // If database not open
if (searchInfo.otpString != null) { if (searchInfo.otpString != null) {
FileDatabaseSelectActivity.launchForSaveResult( FileDatabaseSelectActivity.launchForRegistration(
this, context = this,
searchInfo activityResultLauncher = null,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
) )
} else if (searchShareForMagikeyboard) { } else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForKeyboardSelectionResult( FileDatabaseSelectActivity.launchForSelection(
this, context = this,
searchInfo typeMode = TypeMode.MAGIKEYBOARD,
searchInfo = searchInfo
) )
} else { } else {
FileDatabaseSelectActivity.launchForSearchResult( FileDatabaseSelectActivity.launchForSearchResult(

View File

@@ -0,0 +1,170 @@
package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addHardwareKey
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addSeed
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.buildHardwareKeyChallenge
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.isYubikeyDriverAvailable
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.UIState
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
/**
* Special activity to deal with hardware key drivers,
* return the response to the database service once finished
*/
class HardwareKeyActivity: DatabaseModeActivity(){
private val mHardwareKeyLauncherViewModel: HardwareKeyLauncherViewModel by viewModels()
private var activityResultLauncher: ActivityResultLauncher<Intent> =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
mHardwareKeyLauncherViewModel.manageSelectionResult(it)
}
override fun applyCustomStyle(): Boolean = false
override fun showDatabaseDialog(): Boolean = false
override fun manageDatabaseInfo(): Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
mHardwareKeyLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
is UIState.Loading -> {}
is UIState.ShowHardwareKeyDriverNeeded -> {
showHardwareKeyDriverNeeded(
this@HardwareKeyActivity,
uiState.hardwareKey
) {
mDatabaseViewModel.onChallengeResponded(null)
finish()
}
}
is UIState.LaunchChallengeActivityForResponse -> {
// Send to the driver
activityResultLauncher.launch(
buildHardwareKeyChallenge(uiState.challenge)
)
}
is UIState.OnChallengeResponded -> {
mDatabaseViewModel.onChallengeResponded(uiState.response)
}
}
}
}
lifecycleScope.launch {
mHardwareKeyLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.UIState.ShowError -> {
toastError(uiState.error)
mHardwareKeyLauncherViewModel.cancelResult()
}
else -> {}
}
}
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mHardwareKeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
finish()
}
private fun showHardwareKeyDriverNeeded(
context: Context,
hardwareKey: HardwareKey?,
onDialogDismissed: DialogInterface.OnDismissListener
) {
val builder = AlertDialog.Builder(context)
builder
.setMessage(
context.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
context.openExternalApp(
context.getString(R.string.key_driver_app_id),
context.getString(R.string.key_driver_url)
)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setOnDismissListener(onDialogDismissed)
builder.create().show()
}
companion object {
private val TAG = HardwareKeyActivity::class.java.simpleName
fun launchHardwareKeyActivity(
context: Context,
hardwareKey: HardwareKey,
seed: ByteArray?
) {
context.startActivity(
Intent(
context,
HardwareKeyActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
addHardwareKey(hardwareKey)
addSeed(seed)
})
}
fun isHardwareKeyAvailable(
context: Context,
hardwareKey: HardwareKey?
): Boolean {
if (hardwareKey == null)
return false
return when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
false
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
isYubikeyDriverAvailable(context)
}
}
}
}
}

View File

@@ -36,6 +36,8 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
@@ -44,14 +46,14 @@ import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
@@ -79,10 +81,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
return false return false
} }
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
return false
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
@@ -105,61 +103,69 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
nodeId = uiState.nodeId nodeId = uiState.nodeId
) )
} }
is PasskeyLauncherViewModel.UIState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is PasskeyLauncherViewModel.UIState.ShowError -> {
toastError(uiState.error)
passkeyLauncherViewModel.cancelResult()
}
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForPasskeySelectionResult(
context = this@PasskeyLauncherActivity,
database = uiState.database,
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = null,
autoSearch = false
)
}
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
database = uiState.database,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode
)
}
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
activity = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = uiState.searchInfo,
)
}
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode
)
}
is PasskeyLauncherViewModel.UIState.UpdateEntry -> { is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
updateEntry(uiState.oldEntry, uiState.newEntry) updateEntry(uiState.oldEntry, uiState.newEntry)
} }
} }
} }
} }
lifecycleScope.launch {
passkeyLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.UIState.Loading -> {}
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.UIState.ShowError -> {
toastError(uiState.error)
passkeyLauncherViewModel.cancelResult()
}
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@PasskeyLauncherActivity,
database = uiState.database,
typeMode = uiState.typeMode,
searchInfo = uiState.searchInfo,
activityResultLauncher = mPasskeySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
database = uiState.database,
typeMode = uiState.typeMode,
registerInfo = uiState.registerInfo,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher
)
}
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection(
context = this@PasskeyLauncherActivity,
typeMode = uiState.typeMode,
searchInfo = uiState.searchInfo,
activityResultLauncher = mPasskeySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
typeMode = uiState.typeMode,
registerInfo = uiState.registerInfo,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
)
}
}
}
}
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database) super.onUnknownDatabaseRetrieved(database)
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database) passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -170,7 +176,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
super.onDatabaseActionFinished(database, actionTask, result) super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) { when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> { ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
passkeyLauncherViewModel.autoSelectPasskey(result, database) // TODO When auto save is enabled, WARNING filter by the calling activity
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
} }
} }
} }
@@ -234,6 +241,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
) )
.append("\n\n") .append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation)) .append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
.append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_question))
.toString() .toString()
) )
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
@@ -273,7 +282,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
): PendingIntent? { ): PendingIntent? {
return PendingIntent.getActivity( return PendingIntent.getActivity(
context, context,
(Math.random() * Integer.MAX_VALUE).toInt(), randomRequestCode(),
Intent(context, PasskeyLauncherActivity::class.java).apply { Intent(context, PasskeyLauncherActivity::class.java).apply {
addSpecialMode(specialMode) addSpecialMode(specialMode)
addTypeMode(TypeMode.PASSKEY) addTypeMode(TypeMode.PASSKEY)

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.AppUtil import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import org.joda.time.DateTime import org.joda.time.DateTime
@@ -92,10 +92,11 @@ class KeeAutofillService : AutofillService() {
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this) autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
} }
override fun onFillRequest(request: FillRequest, override fun onFillRequest(
cancellationSignal: CancellationSignal, request: FillRequest,
callback: FillCallback) { cancellationSignal: CancellationSignal,
callback: FillCallback
) {
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") } cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) { if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
@@ -120,67 +121,64 @@ class KeeAutofillService : AutofillService() {
webDomain = parseResult.webDomain webDomain = parseResult.webDomain
webScheme = parseResult.webScheme webScheme = parseResult.webScheme
} }
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain -> val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
searchInfo.webDomain = webDomainWithoutSubDomain && autofillInlineSuggestionsEnabled) {
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R CompatInlineSuggestionsRequest(request)
&& autofillInlineSuggestionsEnabled) { } else {
CompatInlineSuggestionsRequest(request) null
} else {
null
}
launchSelection(mDatabase,
searchInfo,
parseResult,
inlineSuggestionsRequest,
callback)
} }
val autofillComponent = AutofillComponent(
latestStructure,
inlineSuggestionsRequest
)
SearchHelper.checkAutoSearchInfo(
context = this,
database = mDatabase,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
callback.onSuccess(
AutofillHelper.buildResponse(
context = this,
database = openedDatabase,
entriesInfo = items,
parseResult = parseResult,
autofillComponent = autofillComponent
)
)
},
onItemNotFound = { openedDatabase ->
// Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, autofillComponent, callback)
},
onDatabaseClosed = {
// Show UI if database not open
showUIForEntrySelection(parseResult, null,
searchInfo, autofillComponent, callback)
}
)
} }
} }
} }
private fun launchSelection(database: ContextualDatabase?,
searchInfo: SearchInfo,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
callback.onSuccess(
AutofillHelper.buildResponse(
this, openedDatabase,
items, parseResult, inlineSuggestionsRequest
)
)
},
onItemNotFound = { openedDatabase ->
// Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, inlineSuggestionsRequest, callback)
},
onDatabaseClosed = {
// Show UI if database not open
showUIForEntrySelection(parseResult, null,
searchInfo, inlineSuggestionsRequest, callback)
}
)
}
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(
database: ContextualDatabase?, parseResult: StructureParser.Result,
searchInfo: SearchInfo, database: ContextualDatabase?,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?, searchInfo: SearchInfo,
callback: FillCallback) { autofillComponent: AutofillComponent,
callback: FillCallback
) {
var success = false var success = false
parseResult.allAutofillIds().let { autofillIds -> parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) { if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used // If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response. // to generate Response.
AutofillLauncherActivity.getPendingIntentForSelection(this, AutofillLauncherActivity.getPendingIntentForSelection(
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender -> this,
searchInfo,
autofillComponent
)?.intentSender?.let { intentSender ->
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) { val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) { if (!parseResult.webDomain.isNullOrEmpty()) {
@@ -271,7 +269,8 @@ class KeeAutofillService : AutofillService() {
&& autofillInlineSuggestionsEnabled && autofillInlineSuggestionsEnabled
) { ) {
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = val inlinePresentationSpecs =
inlineSuggestionsRequest.inlinePresentationSpecs inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0 if (inlineSuggestionsRequest.maxSuggestionCount > 0
@@ -289,7 +288,7 @@ class KeeAutofillService : AutofillService() {
InlineSuggestionUi.newContentBuilder( InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity( PendingIntent.getActivity(
this, this,
0, randomRequestCode(),
Intent(this, AutofillSettingsActivity::class.java), Intent(this, AutofillSettingsActivity::class.java),
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
) )
@@ -361,7 +360,7 @@ class KeeAutofillService : AutofillService() {
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
var success = false var success = false
if (askToSaveData) { if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val latestStructure = request.fillContexts.last().structure val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse(true)?.let { parseResult -> StructureParser(latestStructure).parse(true)?.let { parseResult ->
@@ -387,32 +386,32 @@ class KeeAutofillService : AutofillService() {
} }
// Show UI to save data // Show UI to save data
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
val registerInfo = RegisterInfo( val registerInfo = RegisterInfo(
searchInfo = SearchInfo().apply { searchInfo = searchInfo,
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
},
username = parseResult.usernameValue?.textValue?.toString(), username = parseResult.usernameValue?.textValue?.toString(),
password = parseResult.passwordValue?.textValue?.toString(), password = parseResult.passwordValue?.textValue?.toString(),
creditCard = creditCard = parseResult.creditCardNumber?.let { cardNumber ->
CreditCard( CreditCard(
parseResult.creditCardHolder, parseResult.creditCardHolder,
parseResult.creditCardNumber, cardNumber,
expiration, expiration,
parseResult.cardVerificationValue parseResult.cardVerificationValue
) )
) }
)
// TODO Callback in each activity #765 AutofillLauncherActivity.getPendingIntentForRegistration(
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { this,
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this, registerInfo
// registerInfo)) )?.intentSender?.let { intentSender ->
//} else { success = true
AutofillLauncherActivity.launchForRegistration(this, registerInfo) callback.onSuccess(intentSender)
success = true }
callback.onSuccess()
//}
} }
} }
} }

View File

@@ -362,8 +362,8 @@ class StructureParser(private val structure: AssistStructure) {
if (result?.passwordId == null) { if (result?.passwordId == null) {
usernameIdCandidate = autofillId usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
} }
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
} }
inputIsVariationType(inputType, inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> { InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {

View File

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

View File

@@ -93,22 +93,18 @@ class PasskeyProviderService : CredentialProviderService() {
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo { private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
return SearchInfo().apply { return SearchInfo().apply {
this.relyingParty = relyingParty this.relyingParty = relyingParty
this.isAPasskeySearch = true
this.query = relyingParty
} }
} }
override fun onBeginGetCredentialRequest( override fun onBeginGetCredentialRequest(
request: BeginGetCredentialRequest, request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal, cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>, callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
) { ) {
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called") Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
try { try {
processGetCredentialsRequest(request)?.let { response -> processGetCredentialsRequest(request) { response ->
callback.onResult(response) callback.onResult(response)
} ?: run {
callback.onError(GetCredentialUnknownException())
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e) Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
@@ -116,24 +112,30 @@ class PasskeyProviderService : CredentialProviderService() {
} }
} }
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? { private fun processGetCredentialsRequest(
val credentialEntries: MutableList<CredentialEntry> = mutableListOf() request: BeginGetCredentialRequest,
callback: (BeginGetCredentialResponse?) -> Unit
) {
var knownOption = false
for (option in request.beginGetCredentialOptions) { for (option in request.beginGetCredentialOptions) {
when (option) { when (option) {
is BeginGetPublicKeyCredentialOption -> { is BeginGetPublicKeyCredentialOption -> {
credentialEntries.addAll( knownOption = true
populatePasskeyData(option) populatePasskeyData(option) { listCredentials ->
) callback(BeginGetCredentialResponse(listCredentials))
return BeginGetCredentialResponse(credentialEntries) }
} }
} }
} }
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption") if (knownOption.not()) {
return null throw IOException("unknown type of beginGetCredentialOption")
}
} }
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> { private fun populatePasskeyData(
option: BeginGetPublicKeyCredentialOption,
callback: (List<CredentialEntry>) -> Unit
) {
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf() val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
@@ -169,6 +171,7 @@ class PasskeyProviderService : CredentialProviderService() {
) )
} }
} }
callback(passkeyEntries)
}, },
onItemNotFound = { _ -> onItemNotFound = { _ ->
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
@@ -191,6 +194,7 @@ class PasskeyProviderService : CredentialProviderService() {
) )
) )
} }
callback(passkeyEntries)
}, },
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Add pending intent for passkey selection in closed database") Log.d(TAG, "Add pending intent for passkey selection in closed database")
@@ -213,9 +217,9 @@ class PasskeyProviderService : CredentialProviderService() {
) )
) )
} }
callback(passkeyEntries)
} }
) )
return passkeyEntries
} }
override fun onBeginCreateCredentialRequest( override fun onBeginCreateCredentialRequest(
@@ -225,7 +229,9 @@ class PasskeyProviderService : CredentialProviderService() {
) { ) {
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called") Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
try { try {
callback.onResult(processCreateCredentialRequest(request)) processCreateCredentialRequest(request) {
callback.onResult(BeginCreateCredentialResponse(it))
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e) Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
toastError(e) toastError(e)
@@ -233,15 +239,20 @@ class PasskeyProviderService : CredentialProviderService() {
} }
} }
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse { private fun processCreateCredentialRequest(
request: BeginCreateCredentialRequest,
callback: (List<CreateEntry>) -> Unit
) {
when (request) { when (request) {
is BeginCreatePublicKeyCredentialRequest -> { is BeginCreatePublicKeyCredentialRequest -> {
// Request is passkey type // Request is passkey type
return handleCreatePasskeyQuery(request) handleCreatePasskeyQuery(request, callback)
}
else -> {
// request type not supported
throw IOException("unknown type of BeginCreateCredentialRequest")
} }
} }
// request type not supported
throw IOException("unknown type of BeginCreateCredentialRequest")
} }
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry( private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
@@ -266,9 +277,15 @@ class PasskeyProviderService : CredentialProviderService() {
} }
} }
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse { private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
val accountName = mDatabase?.name ?: getString(R.string.passkey_database_username) callback: (List<CreateEntry>) -> Unit
) {
val databaseName = mDatabase?.name
val accountName =
if (databaseName?.isBlank() != false)
getString(R.string.passkey_database_username)
else databaseName
val createEntries: MutableList<CreateEntry> = mutableListOf() val createEntries: MutableList<CreateEntry> = mutableListOf()
val relyingPartyId = PublicKeyCredentialCreationOptions( val relyingPartyId = PublicKeyCredentialCreationOptions(
requestJson = request.requestJson, requestJson = request.requestJson,
@@ -309,6 +326,7 @@ class PasskeyProviderService : CredentialProviderService() {
} }
}*/ }*/
} }
callback(createEntries)
}, },
onItemNotFound = { database -> onItemNotFound = { database ->
// To create a new entry // To create a new entry
@@ -317,6 +335,7 @@ class PasskeyProviderService : CredentialProviderService() {
} else { } else {
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo) createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
} }
callback(createEntries)
}, },
onDatabaseClosed = { onDatabaseClosed = {
// Launch the passkey launcher activity to open the database // Launch the passkey launcher activity to open the database
@@ -334,10 +353,9 @@ class PasskeyProviderService : CredentialProviderService() {
) )
) )
} }
callback(createEntries)
} }
) )
return BeginCreateCredentialResponse(createEntries)
} }
override fun onClearCredentialStateRequest( override fun onClearCredentialStateRequest(

View File

@@ -24,7 +24,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.ParcelUuid
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Log import android.util.Log
@@ -44,6 +43,7 @@ import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.Signature import com.kunzisoft.encrypt.Signature
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
@@ -60,7 +60,6 @@ import com.kunzisoft.keepass.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
@@ -88,10 +87,7 @@ object PasskeyHelper {
private const val HMAC_TYPE = "HmacSHA256" private const val HMAC_TYPE = "HmacSHA256"
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin" private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp" private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode" private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
@@ -110,38 +106,6 @@ object PasskeyHelper {
private val internalSecureRandom: SecureRandom = SecureRandom() private val internalSecureRandom: SecureRandom = SecureRandom()
/**
* Build the Passkey response for one entry
*/
fun Activity.buildPasskeyResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
try {
entryInfo.passkey?.let { passkey ->
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.addPasskey(passkey)
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
mReplyIntent.addNodeId(entryInfo.id)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
throw IOException("No passkey found")
}
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the passkey as result", e)
Toast.makeText(
this,
getString(R.string.error_passkey_result),
Toast.LENGTH_SHORT
).show()
setResult(Activity.RESULT_CANCELED)
}
}
/** /**
* Add an authentication code generated by an entry to the intent * Add an authentication code generated by an entry to the intent
*/ */
@@ -181,22 +145,6 @@ object PasskeyHelper {
return this.removeExtra(EXTRA_PASSKEY) return this.removeExtra(EXTRA_PASSKEY)
} }
/**
* Add the search info to the intent
*/
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
searchInfo?.let {
putExtra(EXTRA_SEARCH_INFO, searchInfo)
}
}
/**
* Retrieve the search info from the intent
*/
fun Intent.retrieveSearchInfo(): SearchInfo? {
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
}
/** /**
* Add the app origin to the intent * Add the app origin to the intent
*/ */
@@ -221,21 +169,37 @@ object PasskeyHelper {
} }
/** /**
* Add the node id to the intent, useful for auto passkey selection * Build the Passkey response for one entry
*/ */
fun Intent.addNodeId(nodeId: UUID?) { fun Activity.buildPasskeyResponseAndSetResult(
nodeId?.let { entryInfo: EntryInfo,
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId)) extras: Bundle? = null
) {
try {
entryInfo.passkey?.let { passkey ->
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.addPasskey(passkey)
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
mReplyIntent.addNodeId(entryInfo.id)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
throw IOException("No passkey found")
}
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the passkey as result", e)
Toast.makeText(
this,
getString(R.string.error_passkey_result),
Toast.LENGTH_SHORT
).show()
setResult(Activity.RESULT_CANCELED)
} }
} }
/**
* Retrieve the node id from the intent
*/
fun Intent.retrieveNodeId(): UUID? {
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
}
/** /**
* Check the timestamp and authentication code transmitted via PendingIntent * Check the timestamp and authentication code transmitted via PendingIntent
*/ */
@@ -424,11 +388,15 @@ object PasskeyHelper {
* Utility method to create a passkey and the associated creation request parameters * Utility method to create a passkey and the associated creation request parameters
* [intent] allows to retrieve the request * [intent] allows to retrieve the request
* [context] context to manage package verification files * [context] context to manage package verification files
* [defaultBackupEligibility] the default backup eligibility to add the the passkey entry
* [defaultBackupState] the default backup state to add the the passkey entry
* [passkeyCreated] is called asynchronously when the passkey has been created * [passkeyCreated] is called asynchronously when the passkey has been created
*/ */
suspend fun retrievePasskeyCreationRequestParameters( suspend fun retrievePasskeyCreationRequestParameters(
intent: Intent, intent: Intent,
context: Context, context: Context,
defaultBackupEligibility: Boolean?,
defaultBackupState: Boolean?,
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
) { ) {
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent) val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
@@ -456,7 +424,9 @@ object PasskeyHelper {
privateKeyPem = privateKeyPem, privateKeyPem = privateKeyPem,
credentialId = b64Encode(credentialId), credentialId = b64Encode(credentialId),
userHandle = b64Encode(userHandle), userHandle = b64Encode(userHandle),
relyingParty = relyingParty relyingParty = relyingParty,
backupEligibility = defaultBackupEligibility,
backupState = defaultBackupState
) )
// create new entry in database // create new entry in database
@@ -590,8 +560,8 @@ object PasskeyHelper {
requestOptions: PublicKeyCredentialRequestOptions, requestOptions: PublicKeyCredentialRequestOptions,
clientDataResponse: ClientDataResponse, clientDataResponse: ClientDataResponse,
passkey: Passkey, passkey: Passkey,
backupEligibility: Boolean, defaultBackupEligibility: Boolean,
backupState: Boolean defaultBackupState: Boolean
): PublicKeyCredential { ): PublicKeyCredential {
val getCredentialResponse = FidoPublicKeyCredential( val getCredentialResponse = FidoPublicKeyCredential(
id = passkey.credentialId, id = passkey.credentialId,
@@ -599,8 +569,8 @@ object PasskeyHelper {
requestOptions = requestOptions, requestOptions = requestOptions,
userPresent = true, userPresent = true,
userVerified = true, userVerified = true,
backupEligibility = backupEligibility, backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
backupState = backupState, backupState = passkey.backupState ?: defaultBackupState,
userHandle = passkey.userHandle, userHandle = passkey.userHandle,
privateKey = passkey.privateKeyPem, privateKey = passkey.privateKeyPem,
clientDataResponse = clientDataResponse clientDataResponse = clientDataResponse

View File

@@ -0,0 +1,284 @@
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 com.kunzisoft.keepass.settings.PreferencesUtil
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 mLockDatabaseAfterSelection: Boolean = false
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun initialize() {
mLockDatabaseAfterSelection = PreferencesUtil.isAutofillCloseDatabaseEnable(getApplication())
}
override fun onResult() {
super.onResult()
mAutofillComponent = null
}
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, lockDatabase = mLockDatabaseAfterSelection)
}
},
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
}
}
}
override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult
) {
super.manageSelectionResult(database, activityResult)
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for 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)
}
withContext(Dispatchers.Main) {
AutofillHelper.buildResponse(
context = getApplication(),
autofillComponent = autofillComponent,
database = database,
entriesInfo = entries
) { intent ->
setResult(intent, lockDatabase = mLockDatabaseAfterSelection)
}
}
}
}
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
}
}
override fun manageRegistrationResult(activityResult: ActivityResult) {
isResultLauncherRegistered = false
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 -> {
Log.d(TAG, "Autofill registration 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,151 @@
package com.kunzisoft.keepass.credentialprovider.viewmodel
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.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 isResultLauncherRegistered: Boolean = false
private var mSelectionResult: ActivityResult? = null
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
mSelectionResult = null
}
fun setResult(intent: Intent, lockDatabase: Boolean = false) {
// Remove the launcher register
onResult()
mCredentialUiState.value = UIState.SetActivityResult(
lockDatabase = lockDatabase,
resultCode = RESULT_OK,
data = intent
)
}
fun cancelResult(lockDatabase: Boolean = false) {
onResult()
mCredentialUiState.value = UIState.SetActivityResult(
lockDatabase = lockDatabase,
resultCode = RESULT_CANCELED
)
}
private fun onDatabaseRetrieved(database: ContextualDatabase) {
mDatabase = database
mSelectionResult?.let { selectionResult ->
manageSelectionResult(database, selectionResult)
}
}
fun manageSelectionResult(activityResult: ActivityResult) {
// Waiting for the database if needed
when (activityResult.resultCode) {
RESULT_OK -> {
mSelectionResult = activityResult
mDatabase?.let { database ->
manageSelectionResult(database, activityResult)
}
}
RESULT_CANCELED -> {
cancelResult()
}
}
}
open fun manageSelectionResult(database: ContextualDatabase, activityResult: ActivityResult) {
mSelectionResult = null
}
open fun manageRegistrationResult(activityResult: ActivityResult) {}
open fun onExceptionOccurred(e: Throwable) {
showError(e)
}
open fun launchActionIfNeeded(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
if (database != null) {
onDatabaseRetrieved(database)
}
if (isResultLauncherRegistered.not()) {
isResultLauncherRegistered = true
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
onExceptionOccurred(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

@@ -0,0 +1,147 @@
package com.kunzisoft.keepass.credentialprovider.viewmodel
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity.Companion.isHardwareKeyAvailable
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.hardware.HardwareKey
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class HardwareKeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
override suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
val hardwareKey = HardwareKey.Companion.getHardwareKeyFromString(
intent.getStringExtra(DATA_HARDWARE_KEY)
)
if (isHardwareKeyAvailable(getApplication(), hardwareKey)) {
when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
}
else -> {
UIState.OnChallengeResponded(null)
}
}
} else {
mUiState.value = UIState.ShowHardwareKeyDriverNeeded(hardwareKey)
}
}
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
mUiState.value = UIState.LaunchChallengeActivityForResponse(challenge)
Log.d(TAG, "Challenge sent")
}
override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult
) {
super.manageSelectionResult(database, activityResult)
if (activityResult.resultCode == RESULT_OK) {
val challengeResponse: ByteArray? =
activityResult.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
mUiState.value = UIState.OnChallengeResponded(challengeResponse)
} else {
Log.e(TAG, "Response from challenge error")
mUiState.value = UIState.OnChallengeResponded(null)
}
}
sealed class UIState {
object Loading : UIState()
data class ShowHardwareKeyDriverNeeded(
val hardwareKey: HardwareKey?
): UIState()
data class LaunchChallengeActivityForResponse(
val challenge: ByteArray?,
): UIState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LaunchChallengeActivityForResponse
return challenge.contentEquals(other.challenge)
}
override fun hashCode(): Int {
return challenge?.contentHashCode() ?: 0
}
}
data class OnChallengeResponded(
val response: ByteArray?
): UIState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as OnChallengeResponded
return response.contentEquals(other.response)
}
override fun hashCode(): Int {
return response?.contentHashCode() ?: 0
}
}
}
companion object {
private val TAG = HardwareKeyLauncherViewModel::class.java.name
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
private const val DATA_SEED = "DATA_SEED"
// Driver call
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
fun isYubikeyDriverAvailable(context: Context): Boolean {
return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(context.packageManager) != null
}
fun buildHardwareKeyChallenge(challenge: ByteArray?): Intent {
return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
}
fun Intent.addHardwareKey(hardwareKey: HardwareKey) {
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
}
fun Intent.addSeed(seed: ByteArray?) {
putExtra(DATA_SEED, seed)
}
}
}

View File

@@ -11,8 +11,11 @@ import androidx.annotation.RequiresApi
import androidx.credentials.GetCredentialResponse import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialUnknownException import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PendingIntentHandler
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
@@ -25,11 +28,9 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVe
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
@@ -56,22 +57,21 @@ import java.io.InvalidObjectException
import java.util.UUID import java.util.UUID
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class PasskeyLauncherViewModel(application: Application): AndroidViewModel(application) { class PasskeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
private var mPasskey: Passkey? = null private var mPasskey: Passkey? = null
private var mLockDatabaseAfterSelection: Boolean = false
private var mBackupEligibility: Boolean = true private var mBackupEligibility: Boolean = true
private var mBackupState: Boolean = false private var mBackupState: Boolean = false
private var mLockDatabase: Boolean = true
private var isResultLauncherRegistered: Boolean = false private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = _uiState
fun initialize() { fun initialize() {
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication()) mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication()) mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
} }
@@ -79,19 +79,14 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
fun showAppPrivilegedDialog( fun showAppPrivilegedDialog(
temptingApp: AndroidPrivilegedApp temptingApp: AndroidPrivilegedApp
) { ) {
_uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp) mUiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
} }
fun showAppSignatureDialog( fun showAppSignatureDialog(
temptingApp: AppOrigin, temptingApp: AppOrigin,
nodeId: UUID nodeId: UUID
) { ) {
_uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId) mUiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
}
fun showError(error: Throwable) {
Log.e(TAG, "Error on passkey launch", error)
_uiState.value = UIState.ShowError(error)
} }
fun saveCustomPrivilegedApp( fun saveCustomPrivilegedApp(
@@ -107,7 +102,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
context = getApplication(), context = getApplication(),
privilegedApps = listOf(temptingApp) privilegedApps = listOf(temptingApp)
) )
launchPasskeyAction( launchAction(
intent = intent, intent = intent,
specialMode = specialMode, specialMode = specialMode,
database = database database = database
@@ -139,54 +134,33 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
) )
entryInfo.saveAppOrigin(database, temptingApp) entryInfo.saveAppOrigin(database, temptingApp)
newEntry.setEntryInfo(database, entryInfo) newEntry.setEntryInfo(database, entryInfo)
_uiState.value = UIState.UpdateEntry( mUiState.value = UIState.UpdateEntry(
oldEntry = entry, oldEntry = entry,
newEntry = newEntry newEntry = newEntry
) )
} }
} }
fun setResult(intent: Intent) { override fun onExceptionOccurred(e: Throwable) {
// Remove the launcher register if (e is PrivilegedAllowLists.PrivilegedException) {
isResultLauncherRegistered = false showAppPrivilegedDialog(e.temptingApp)
_uiState.value = UIState.SetActivityResult( } else {
lockDatabase = mLockDatabase, super.onExceptionOccurred(e)
resultCode = RESULT_OK, }
data = intent
)
} }
fun cancelResult() { override fun launchActionIfNeeded(
isResultLauncherRegistered = false
_uiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_CANCELED
)
}
fun launchPasskeyActionIfNeeded(
intent: Intent, intent: Intent,
specialMode: SpecialMode, specialMode: SpecialMode,
database: ContextualDatabase? database: ContextualDatabase?
) { ) {
if (isResultLauncherRegistered.not()) { // Launch with database when a nodeId is present
isResultLauncherRegistered = true if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
viewModelScope.launch(CoroutineExceptionHandler { _, e -> super.launchActionIfNeeded(intent, specialMode, database)
if (e is PrivilegedAllowLists.PrivilegedException) {
showAppPrivilegedDialog(e.temptingApp)
} else {
showError(e)
}
}) {
launchPasskeyAction(intent, specialMode, database)
}
} }
} }
/** override suspend fun launchAction(
* Launch the main action to manage Passkey
*/
private suspend fun launchPasskeyAction(
intent: Intent, intent: Intent,
specialMode: SpecialMode, specialMode: SpecialMode,
database: ContextualDatabase? database: ContextualDatabase?
@@ -194,6 +168,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo() val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false) val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
val nodeId = intent.retrieveNodeId() val nodeId = intent.retrieveNodeId()
intent.removeInfo()
intent.removeAppOrigin()
intent.removeNodeId()
checkSecurity(intent, nodeId) checkSecurity(intent, nodeId)
when (specialMode) { when (specialMode) {
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
@@ -260,15 +237,19 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "No Passkey found for selection," + TAG, "No Passkey found for selection," +
"launch manual selection in opened database" "launch manual selection in opened database"
) )
_uiState.value = UIState.LaunchGroupActivityForSelection( mCredentialUiState.value =
database = openedDatabase CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
) database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
)
}, },
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database") Log.d(TAG, "Manual passkey selection in closed database")
_uiState.value = mCredentialUiState.value =
UIState.LaunchFileDatabaseSelectActivityForSelection( CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
) )
} }
) )
@@ -326,12 +307,12 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
appOrigin = appOrigin appOrigin = appOrigin
), ),
passkey = passkey, passkey = passkey,
backupEligibility = mBackupEligibility, defaultBackupEligibility = mBackupEligibility,
backupState = mBackupState defaultBackupState = mBackupState
) )
) )
) )
setResult(result) setResult(result, lockDatabase = mLockDatabaseAfterSelection)
} catch (e: SignatureNotFoundException) { } catch (e: SignatureNotFoundException) {
// Request the dialog if signature exception // Request the dialog if signature exception
showAppSignatureDialog(e.temptingApp, nodeId) showAppSignatureDialog(e.temptingApp, nodeId)
@@ -340,9 +321,11 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
} }
} }
fun manageSelectionResult( override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult activityResult: ActivityResult
) { ) {
super.manageSelectionResult(database, activityResult)
val intent = activityResult.data val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e -> viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for passkey", e) Log.e(TAG, "Unable to create selection response for passkey", e)
@@ -380,8 +363,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
appOrigin = appOrigin appOrigin = appOrigin
), ),
passkey = passkey, passkey = passkey,
backupEligibility = mBackupEligibility, defaultBackupEligibility = mBackupEligibility,
backupState = mBackupState defaultBackupState = mBackupState
) )
) )
) )
@@ -389,7 +372,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
throw IOException("Usage parameters is null") throw IOException("Usage parameters is null")
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
setResult(responseIntent) setResult(responseIntent, lockDatabase = mLockDatabaseAfterSelection)
} }
} }
} }
@@ -417,6 +400,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
retrievePasskeyCreationRequestParameters( retrievePasskeyCreationRequestParameters(
intent = intent, intent = intent,
context = getApplication(), context = getApplication(),
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState,
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters -> passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
// Save the requested parameters // Save the requested parameters
mPasskey = passkey mPasskey = passkey
@@ -440,24 +425,26 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "Passkey found for registration, " + TAG, "Passkey found for registration, " +
"but launch manual registration for a new entry" "but launch manual registration for a new entry"
) )
_uiState.value = UIState.LaunchGroupActivityForRegistration( mCredentialUiState.value =
database = openedDatabase, CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
registerInfo = registerInfo, database = openedDatabase,
typeMode = TypeMode.PASSKEY registerInfo = registerInfo,
) typeMode = TypeMode.PASSKEY
)
}, },
onItemNotFound = { openedDatabase -> onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database") Log.d(TAG, "Launch new manual registration in opened database")
_uiState.value = UIState.LaunchGroupActivityForRegistration( mCredentialUiState.value =
database = openedDatabase, CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
registerInfo = registerInfo, database = openedDatabase,
typeMode = TypeMode.PASSKEY registerInfo = registerInfo,
) typeMode = TypeMode.PASSKEY
)
}, },
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Manual passkey registration in closed database") Log.d(TAG, "Manual passkey registration in closed database")
_uiState.value = mCredentialUiState.value =
UIState.LaunchFileDatabaseSelectActivityForRegistration( CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo, registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY typeMode = TypeMode.PASSKEY
) )
@@ -490,7 +477,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
} }
} }
fun manageRegistrationResult(activityResult: ActivityResult) { override fun manageRegistrationResult(activityResult: ActivityResult) {
val intent = activityResult.data val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e -> viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create registration response for passkey", e) Log.e(TAG, "Unable to create registration response for passkey", e)
@@ -518,8 +505,10 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
intent = responseIntent, intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse( response = buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters = it, publicKeyCredentialCreationParameters = it,
backupEligibility = mBackupEligibility, backupEligibility = passkey?.backupEligibility
backupState = mBackupState ?: mBackupEligibility,
backupState = passkey?.backupState
?: mBackupState
) )
) )
} }
@@ -549,29 +538,6 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val temptingApp: AppOrigin, val temptingApp: AppOrigin,
val nodeId: UUID val nodeId: UUID
): UIState() ): UIState()
data class LaunchGroupActivityForSelection(
val database: ContextualDatabase
): UIState()
data class LaunchGroupActivityForRegistration(
val database: ContextualDatabase,
val registerInfo: RegisterInfo,
val typeMode: TypeMode
): UIState()
data class LaunchFileDatabaseSelectActivityForSelection(
val searchInfo: SearchInfo
): 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()
data class UpdateEntry( data class UpdateEntry(
val oldEntry: Entry, val oldEntry: Entry,
val newEntry: Entry val newEntry: Entry

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.database package com.kunzisoft.keepass.database
import android.Manifest
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@@ -29,23 +28,15 @@ import android.content.Context.BIND_IMPORTANT
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
@@ -55,7 +46,6 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
@@ -89,13 +79,9 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.putParcelableList import com.kunzisoft.keepass.utils.putParcelableList
import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
/** /**
@@ -103,121 +89,29 @@ import java.util.UUID
* Useful to retrieve a database instance and sending tasks commands * Useful to retrieve a database instance and sending tasks commands
*/ */
class DatabaseTaskProvider( class DatabaseTaskProvider(
private var context: Context, private var context: Context
private var showDialog: Boolean = true
) { ) {
// To show dialog only if context is an activity
private var activity: FragmentActivity? = try {
context as? FragmentActivity?
} catch (_: Exception) {
null
}
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
var onActionFinish: (( var onStartActionRequested: ((bundle: Bundle?, actionTask: String) -> Unit)? = null
database: ContextualDatabase, var actionTaskListener: DatabaseTaskNotificationService.ActionTaskListener? = null
actionTask: String, var databaseInfoListener: DatabaseTaskNotificationService.DatabaseInfoListener? = null
result: ActionRunnable.Result
) -> Unit)? = null
private var intentDatabaseTask: Intent = Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
)
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
private var serviceConnection: ServiceConnection? = null private var serviceConnection: ServiceConnection? = null
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
fun destroy() { fun destroy() {
this.activity = null
this.onDatabaseRetrieved = null this.onDatabaseRetrieved = null
this.onActionFinish = null
this.databaseTaskBroadcastReceiver = null this.databaseTaskBroadcastReceiver = null
this.mBinder = null this.mBinder = null
this.serviceConnection = null this.serviceConnection = null
this.progressTaskDialogFragment = null
this.databaseChangedDialogFragment = null
} }
private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener { fun onDatabaseChangeValidated() {
override fun onActionStarted( mBinder?.getService()?.saveDatabaseInfo()
database: ContextualDatabase,
progressMessage: ProgressMessage
) {
if (showDialog)
startDialog(progressMessage)
}
override fun onActionUpdated(
database: ContextualDatabase,
progressMessage: ProgressMessage
) {
if (showDialog)
updateDialog(progressMessage)
}
override fun onActionStopped(
database: ContextualDatabase
) {
// Remove the progress task
stopDialog()
}
override fun onActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
onActionFinish?.invoke(database, actionTask, result)
onActionStopped(database)
}
}
private val mActionDatabaseListener =
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
override fun validateDatabaseChanged() {
mBinder?.getService()?.saveDatabaseInfo()
}
}
private var databaseInfoListener = object :
DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(
previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo,
readOnlyDatabase: Boolean
) {
activity?.let { activity ->
activity.lifecycleScope.launch {
if (databaseChangedDialogFragment == null) {
databaseChangedDialogFragment = activity.supportFragmentManager
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
}
if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
previousDatabaseInfo,
newDatabaseInfo,
readOnlyDatabase
)
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
databaseChangedDialogFragment?.show(
activity.supportFragmentManager,
DATABASE_CHANGED_DIALOG_TAG
)
}
}
}
}
} }
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener { private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
@@ -226,48 +120,18 @@ class DatabaseTaskProvider(
} }
} }
private fun startDialog(progressMessage: ProgressMessage) {
activity?.let { activity ->
activity.lifecycleScope.launch {
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = activity.supportFragmentManager
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
}
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(
activity.supportFragmentManager,
PROGRESS_TASK_DIALOG_TAG
)
}
updateDialog(progressMessage)
}
}
}
private fun updateDialog(progressMessage: ProgressMessage) {
progressTaskDialogFragment?.apply {
updateTitle(progressMessage.titleId)
updateMessage(progressMessage.messageId)
updateWarning(progressMessage.warningId)
setCancellable(progressMessage.cancelable)
}
}
private fun stopDialog() {
progressTaskDialogFragment?.dismissAllowingStateLoss()
progressTaskDialogFragment = null
}
private fun initServiceConnection() { private fun initServiceConnection() {
actionTaskListener?.onActionStopped()
if (serviceConnection == null) { if (serviceConnection == null) {
serviceConnection = object : ServiceConnection { serviceConnection = object : ServiceConnection {
override fun onBindingDied(name: ComponentName?) { override fun onBindingDied(name: ComponentName?) {
stopDialog() actionTaskListener?.onActionStopped()
onDatabaseRetrieved?.invoke(null)
} }
override fun onNullBinding(name: ComponentName?) { override fun onNullBinding(name: ComponentName?) {
stopDialog() actionTaskListener?.onActionStopped()
onDatabaseRetrieved?.invoke(null)
} }
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
@@ -290,21 +154,33 @@ class DatabaseTaskProvider(
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) { private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.addDatabaseListener(databaseListener) service?.addDatabaseListener(databaseListener)
service?.addDatabaseFileInfoListener(databaseInfoListener) databaseInfoListener?.let { infoListener ->
service?.addActionTaskListener(actionTaskListener) service?.addDatabaseFileInfoListener(infoListener)
}
actionTaskListener?.let { taskListener ->
service?.addActionTaskListener(taskListener)
}
} }
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) { private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.removeActionTaskListener(actionTaskListener) actionTaskListener?.let { taskListener ->
service?.removeDatabaseFileInfoListener(databaseInfoListener) service?.removeActionTaskListener(taskListener)
}
databaseInfoListener?.let { infoListener ->
service?.removeDatabaseFileInfoListener(infoListener)
}
service?.removeDatabaseListener(databaseListener) service?.removeDatabaseListener(databaseListener)
onDatabaseRetrieved?.invoke(null)
} }
private fun bindService() { private fun bindService() {
initServiceConnection() initServiceConnection()
serviceConnection?.let { serviceConnection?.let {
context.bindService( context.bindService(
intentDatabaseTask, Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
),
it, it,
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
) )
@@ -368,58 +244,9 @@ class DatabaseTaskProvider(
} }
} }
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
private val requestPermissionLauncher = activity?.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { _ ->
// Whether or not the user has accepted, the service can be started,
// There just won't be any notification if it's not allowed.
tempServiceParameters.removeFirstOrNull()?.let {
startService(it.first, it.second)
}
}
private fun start(bundle: Bundle? = null, actionTask: String) { private fun start(bundle: Bundle? = null, actionTask: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { onStartActionRequested?.invoke(bundle, actionTask) ?: run {
val contextActivity = activity context.startDatabaseService(bundle, actionTask)
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED
) {
startService(bundle, actionTask)
} else if (contextActivity != null && shouldShowRequestPermissionRationale(
contextActivity,
Manifest.permission.POST_NOTIFICATIONS
)
) {
// it's not the first time, so the user deliberately chooses not to display the notification
startService(bundle, actionTask)
} else {
AlertDialog.Builder(context)
.setMessage(R.string.warning_database_notification_permission)
.setNegativeButton(R.string.later) { _, _ ->
// Refuses the notification, so start the service
startService(bundle, actionTask)
}
.setPositiveButton(R.string.ask) { _, _ ->
// Save the temp parameters to ask the permission
tempServiceParameters.add(Pair(bundle, actionTask))
requestPermissionLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
}.create().show()
}
} else {
startService(bundle, actionTask)
}
}
private fun startService(bundle: Bundle? = null, actionTask: String) {
try {
if (bundle != null)
intentDatabaseTask.putExtras(bundle)
intentDatabaseTask.action = actionTask
context.startService(intentDatabaseTask)
} catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e)
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
} }
} }
@@ -842,5 +669,21 @@ class DatabaseTaskProvider(
companion object { companion object {
private val TAG = DatabaseTaskProvider::class.java.name private val TAG = DatabaseTaskProvider::class.java.name
fun Context.startDatabaseService(bundle: Bundle? = null, actionTask: String) {
try {
val intentDatabaseTask = Intent(
applicationContext,
DatabaseTaskNotificationService::class.java
)
if (bundle != null)
intentDatabaseTask.putExtras(bundle)
intentDatabaseTask.action = actionTask
startService(intentDatabaseTask)
} catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e)
Toast.makeText(this, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
}
}
} }
} }

View File

@@ -47,6 +47,8 @@ import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.database.exception.VersionDatabaseException import com.kunzisoft.keepass.database.exception.VersionDatabaseException
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BE
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BS
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
@@ -146,6 +148,8 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id) FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle) FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party) FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
FIELD_FLAG_BE.equals(name, true) -> context.getString(R.string.passkey_backup_eligibility)
FIELD_FLAG_BS.equals(name, true) -> context.getString(R.string.passkey_backup_state)
else -> name else -> name
} }

View File

@@ -21,9 +21,16 @@ package com.kunzisoft.keepass.database.helper
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
object SearchHelper { object SearchHelper {
@@ -40,6 +47,76 @@ object SearchHelper {
} }
} }
/**
* Get the concrete web domain AKA without sub domain if needed
*/
private fun getConcreteWebDomain(
context: Context,
webDomain: String?,
concreteWebDomain: (searchSubDomains: Boolean, concreteWebDomain: String?) -> Unit
) {
val domain = webDomain
val searchSubDomains = searchSubDomains(context)
if (domain != null) {
// Warning, web domain can contains IP, don't crop in this case
if (searchSubDomains
|| Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
concreteWebDomain.invoke(searchSubDomains, webDomain)
} else {
CoroutineScope(Dispatchers.IO).launch {
val publicSuffixList = PublicSuffixList(context)
val publicSuffix = publicSuffixList
.getPublicSuffixPlusOne(domain).await()
withContext(Dispatchers.Main) {
concreteWebDomain.invoke(false, publicSuffix)
}
}
}
} else {
concreteWebDomain.invoke(searchSubDomains, null)
}
}
/**
* Create search parameters asynchronously from [SearchInfo]
*/
fun SearchInfo.getSearchParametersFromSearchInfo(
context: Context,
callback: (SearchParameters) -> Unit
) {
getConcreteWebDomain(
context,
webDomain
) { searchSubDomains, concreteDomain ->
var query = this.toString()
if (isDomainSearch && concreteDomain != null)
query = concreteDomain
callback.invoke(
SearchParameters().apply {
searchQuery = query
allowEmptyQuery = false
searchInTitles = false
searchInUsernames = false
searchInPasswords = false
searchInAppIds = isAppIdSearch
searchInUrls = isDomainSearch
searchByDomain = true
searchBySubDomain = searchSubDomains
searchInRelyingParty = isPasskeySearch
searchInNotes = false
searchInOTP = isOTPSearch
searchInOther = false
searchInUUIDs = false
searchInTags = isTagSearch
searchInCurrentGroup = false
searchInSearchableGroup = true
searchInRecycleBin = false
searchInTemplates = false
}
)
}
}
/** /**
* Utility method to perform actions if item is found or not after an auto search in [database] * Utility method to perform actions if item is found or not after an auto search in [database]
*/ */
@@ -52,28 +129,31 @@ object SearchHelper {
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit, onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
onDatabaseClosed: () -> Unit onDatabaseClosed: () -> Unit
) { ) {
// Do not place coroutine at start, bug in Passkey implementation
if (database == null || !database.loaded) { if (database == null || !database.loaded) {
onDatabaseClosed.invoke() onDatabaseClosed.invoke()
} else if (TimeoutHelper.checkTime(context)) { } else if (TimeoutHelper.checkTime(context)) {
var searchWithoutUI = false
if (searchInfo != null if (searchInfo != null
&& !searchInfo.manualSelection && !searchInfo.manualSelection
&& !searchInfo.containsOnlyNullValues()) { && !searchInfo.containsOnlyNullValues()
// If search provide results ) {
database.createVirtualGroupFromSearchInfo( searchInfo.getSearchParametersFromSearchInfo(context) { searchParameters ->
searchInfo, // If search provide results
MAX_SEARCH_ENTRY database.createVirtualGroupFromSearchInfo(
)?.let { searchGroup -> searchParameters = searchParameters,
if (searchGroup.numberOfChildEntries > 0) { max = MAX_SEARCH_ENTRY
searchWithoutUI = true )?.let { searchGroup ->
onItemsFound.invoke(database, if (searchGroup.numberOfChildEntries > 0) {
searchGroup.getChildEntriesInfo(database)) onItemsFound.invoke(
} database,
searchGroup.getChildEntriesInfo(database)
)
} else
onItemNotFound.invoke(database)
} ?: onItemNotFound.invoke(database)
} }
} } else
if (!searchWithoutUI) {
onItemNotFound.invoke(database) onItemNotFound.invoke(database)
}
} }
} }
} }

View File

@@ -1,170 +0,0 @@
package com.kunzisoft.keepass.hardware
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
/**
* Special activity to deal with hardware key drivers,
* return the response to the database service once finished
*/
class HardwareKeyActivity: DatabaseModeActivity(){
// To manage hardware key challenge response
private val resultCallback = ActivityResultCallback<ActivityResult> { result ->
if (result.resultCode == Activity.RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
mDatabaseTaskProvider?.startChallengeResponded(challengeResponse ?: ByteArray(0))
} else {
Log.e(TAG, "Response from challenge error")
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
}
finish()
}
private var activityResultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
override fun applyCustomStyle(): Boolean {
return false
}
override fun showDatabaseDialog(): Boolean {
return false
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
val hardwareKey = HardwareKey.getHardwareKeyFromString(
intent.getStringExtra(DATA_HARDWARE_KEY)
)
if (isHardwareKeyAvailable(this, hardwareKey, true) {
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
}) {
when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
}
else -> {
finish()
}
}
}
}
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
// Send to the driver
activityResultLauncher.launch(
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
)
Log.d(TAG, "Challenge sent")
}
companion object {
private val TAG = HardwareKeyActivity::class.java.simpleName
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
private const val DATA_SEED = "DATA_SEED"
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
fun launchHardwareKeyActivity(
context: Context,
hardwareKey: HardwareKey,
seed: ByteArray?
) {
context.startActivity(Intent(context, HardwareKeyActivity::class.java).apply {
flags = FLAG_ACTIVITY_NEW_TASK
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
putExtra(DATA_SEED, seed)
})
}
fun isHardwareKeyAvailable(
context: Context,
hardwareKey: HardwareKey?,
showDialog: Boolean = true,
onDialogDismissed: DialogInterface.OnDismissListener? = null
): Boolean {
if (hardwareKey == null)
return false
return when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
if (showDialog)
UnderDevelopmentFeatureDialogFragment()
.show(activity.supportFragmentManager, "underDevFeatureDialog")
false
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
val yubikeyDriverAvailable =
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(context.packageManager) != null
if (showDialog && !yubikeyDriverAvailable
&& context is Activity)
showHardwareKeyDriverNeeded(context, hardwareKey) {
onDialogDismissed?.onDismiss(it)
context.finish()
}
yubikeyDriverAvailable
}
}
}
private fun showHardwareKeyDriverNeeded(
context: Context,
hardwareKey: HardwareKey,
onDialogDismissed: DialogInterface.OnDismissListener
) {
val builder = AlertDialog.Builder(context)
builder
.setMessage(
context.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
context.openExternalApp(
context.getString(R.string.key_driver_app_id),
context.getString(R.string.key_driver_url)
)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setOnDismissListener(onDialogDismissed)
builder.create().show()
}
}
}

View File

@@ -33,20 +33,22 @@ import java.util.*
class PasswordGenerator(private val resources: Resources) { class PasswordGenerator(private val resources: Resources) {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun generatePassword(length: Int, fun generatePassword(
upperCase: Boolean, length: Int,
lowerCase: Boolean, upperCase: Boolean,
digits: Boolean, lowerCase: Boolean,
minus: Boolean, digits: Boolean,
underline: Boolean, minus: Boolean,
space: Boolean, underline: Boolean,
specials: Boolean, space: Boolean,
brackets: Boolean, specials: Boolean,
extended: Boolean, brackets: Boolean,
considerChars: String, extended: Boolean,
ignoreChars: String, considerChars: String,
atLeastOneFromEach: Boolean, ignoreChars: String,
excludeAmbiguousChar: Boolean): String { atLeastOneFromEach: Boolean,
excludeAmbiguousChar: Boolean
): String {
// Desired password length is 0 or less // Desired password length is 0 or less
if (length <= 0) { if (length <= 0) {
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length)) throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
@@ -228,7 +230,7 @@ class PasswordGenerator(private val resources: Resources) {
private const val MINUS_CHAR = "-" private const val MINUS_CHAR = "-"
private const val UNDERLINE_CHAR = "_" private const val UNDERLINE_CHAR = "_"
private const val SPACE_CHAR = " " private const val SPACE_CHAR = " "
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`" private const val SPECIAL_CHARS = "&/,^@.#:%\\='$!?*`;+\"|~"
private const val BRACKET_CHARS = "[]{}()<>" private const val BRACKET_CHARS = "[]{}()<>"
private const val AMBIGUOUS_CHARS = "iI|lLoO01" private const val AMBIGUOUS_CHARS = "iI|lLoO01"

View File

@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -194,7 +195,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
private fun newNotification(attachmentNotification: AttachmentNotification) { private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this, val pendingContentIntent = PendingIntent.getActivity(this,
0, randomRequestCode(),
Intent().apply { Intent().apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
setDataAndType(attachmentNotification.uri, setDataAndType(attachmentNotification.uri,
@@ -208,7 +209,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
) )
val pendingDeleteIntent = PendingIntent.getService(this, val pendingDeleteIntent = PendingIntent.getService(this,
0, randomRequestCode(),
Intent(this, AttachmentFileNotificationService::class.java).apply { Intent(this, AttachmentFileNotificationService::class.java).apply {
// No action to delete the service // No action to delete the service
putExtra(FILE_URI_KEY, attachmentNotification.uri) putExtra(FILE_URI_KEY, attachmentNotification.uri)

View File

@@ -61,13 +61,14 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
@@ -175,7 +176,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
progressMessage: ProgressMessage progressMessage: ProgressMessage
) )
fun onActionStopped( fun onActionStopped(
database: ContextualDatabase database: ContextualDatabase? = null
) )
fun onActionFinished( fun onActionFinished(
database: ContextualDatabase, database: ContextualDatabase,
@@ -550,7 +551,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Build Intents for notification action // Build Intents for notification action
val pendingDatabaseIntent = PendingIntent.getActivity( val pendingDatabaseIntent = PendingIntent.getActivity(
this, this,
0, randomRequestCode(),
Intent(this, GroupActivity::class.java), Intent(this, GroupActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
@@ -675,6 +676,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
override fun actionOnLock() { override fun actionOnLock() {
if (!TimeoutHelper.temporarilyDisableLock) { if (!TimeoutHelper.temporarilyDisableLock) {
closeDatabase(mDatabase) closeDatabase(mDatabase)
// Remove the database during the lock
// And notify each subscriber
mDatabase = null
mDatabaseListeners.forEach { listener ->
listener.onDatabaseRetrieved(null)
}
// Remove the lock timer (no more needed if it exists) // Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this) TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast // Service is stopped after receive the broadcast
@@ -709,9 +716,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
notifyProgressMessage() notifyProgressMessage()
HardwareKeyActivity HardwareKeyActivity
.launchHardwareKeyActivity( .launchHardwareKeyActivity(
this@DatabaseTaskNotificationService, context = this@DatabaseTaskNotificationService,
hardwareKey, hardwareKey = hardwareKey,
seed seed = seed
) )
// Wait the response // Wait the response
mProgressMessage.apply { mProgressMessage.apply {

View File

@@ -41,6 +41,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
autofillInlineSuggestionsPreference?.isVisible = false autofillInlineSuggestionsPreference?.isVisible = false
} }
val autofillAskSaveDataPreference: TwoStatePreference? = findPreference(getString(R.string.autofill_ask_to_save_data_key))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
autofillAskSaveDataPreference?.isVisible = false
}
} }
override fun onDisplayPreferenceDialog(preference: Preference) { override fun onDisplayPreferenceDialog(preference: Preference) {

View File

@@ -21,20 +21,25 @@ package com.kunzisoft.keepass.settings
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
class MainPreferenceFragment : PreferenceFragmentCompat() { class MainPreferenceFragment : PreferenceFragmentCompat() {
private var mCallback: Callback? = null private var mCallback: Callback? = null
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabaseLoaded: Boolean = false private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@@ -50,20 +55,24 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
mCallback = null mCallback = null
super.onDetach() super.onDetach()
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database -> override fun onCreate(savedInstanceState: Bundle?) {
mDatabaseLoaded = database?.loaded == true super.onCreate(savedInstanceState)
checkDatabaseLoaded() lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
checkDatabaseLoaded(database?.loaded == true)
}
}
} }
super.onViewCreated(view, savedInstanceState)
} }
private fun checkDatabaseLoaded() { private fun checkDatabaseLoaded(isDatabaseLoaded: Boolean) {
findPreference<Preference>(getString(R.string.settings_database_key)) findPreference<Preference>(getString(R.string.settings_database_key))
?.isEnabled = mDatabaseLoaded ?.isEnabled = isDatabaseLoaded
findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key)) findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key))
?.isVisible = mDatabaseLoaded ?.isVisible = isDatabaseLoaded
} }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -119,7 +128,7 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
} }
} }
checkDatabaseLoaded() checkDatabaseLoaded(mDatabase?.loaded == true)
} }
interface Callback { interface Callback {

View File

@@ -19,13 +19,21 @@
*/ */
package com.kunzisoft.keepass.settings package com.kunzisoft.keepass.settings
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.* import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.TwoStatePreference import androidx.preference.TwoStatePreference
@@ -39,19 +47,40 @@ import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.helper.* import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.preference.* import com.kunzisoft.keepass.settings.preference.DialogColorPreference
import com.kunzisoft.keepass.settings.preferencedialogfragment.* import com.kunzisoft.keepass.settings.preference.DialogListExplanationPreference
import com.kunzisoft.keepass.settings.preference.InputKdfNumberPreference
import com.kunzisoft.keepass.settings.preference.InputKdfSizePreference
import com.kunzisoft.keepass.settings.preference.InputNumberPreference
import com.kunzisoft.keepass.settings.preference.InputTextPreference
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseColorPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDataCompressionPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDefaultUsernamePreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDescriptionPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseKeyDerivationPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistorySizePreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMemoryUsagePreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseNamePreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseParallelismPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRoundsPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseTemplatesGroupPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getParcelableCompat import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getSerializableCompat import com.kunzisoft.keepass.utils.getSerializableCompat
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval { class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: ContextualDatabase? = null private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
private var mDatabaseReadOnly: Boolean = false private var mDatabaseReadOnly: Boolean = false
private var mMergeDataAllowed: Boolean = false private var mMergeDataAllowed: Boolean = false
private var mDatabaseAutoSaveEnabled: Boolean = true private var mDatabaseAutoSaveEnabled: Boolean = true
@@ -114,19 +143,46 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner) activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
viewLifecycleOwner.lifecycleScope.launch {
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database -> mDatabaseViewModel.databaseState.collect { database ->
mDatabase = database view.resetAppTimeoutWhenViewTouchedOrFocused(
view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded) context = requireContext(),
onDatabaseRetrieved(database) databaseLoaded = database?.loaded
} )
}
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
onDatabaseActionFinished(it.database, it.actionTask, it.result)
} }
} }
@@ -167,29 +223,26 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabaseViewModel.reloadDatabase(false) mDatabaseViewModel.reloadDatabase(false)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
mDatabase = database mDatabaseReadOnly = database.isReadOnly
mDatabaseReadOnly = database?.isReadOnly == true mMergeDataAllowed = database.isMergeDataAllowed()
mMergeDataAllowed = database?.isMergeDataAllowed() == true
mDatabase?.let { if (database.loaded) {
if (it.loaded) { when (mScreen) {
when (mScreen) { Screen.DATABASE -> {
Screen.DATABASE -> { onCreateDatabasePreference(database)
onCreateDatabasePreference(it) }
} Screen.DATABASE_SECURITY -> {
Screen.DATABASE_SECURITY -> { onCreateDatabaseSecurityPreference(database)
onCreateDatabaseSecurityPreference(it) }
} Screen.DATABASE_MASTER_KEY -> {
Screen.DATABASE_MASTER_KEY -> { onCreateDatabaseMasterKeyPreference(database)
onCreateDatabaseMasterKeyPreference(it) }
} else -> {
else -> {
}
} }
} else {
Log.e(javaClass.name, "Database isn't ready")
} }
} else {
Log.e(javaClass.name, "Database isn't ready")
} }
} }
@@ -458,7 +511,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newDefaultUsername newDefaultUsername
} else { } else {
mDatabase?.defaultUsername = oldDefaultUsername database.defaultUsername = oldDefaultUsername
oldDefaultUsername oldDefaultUsername
} }
dbDefaultUsernamePref?.summary = defaultUsernameToShow dbDefaultUsernamePref?.summary = defaultUsernameToShow
@@ -471,7 +524,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newColor newColor
} else { } else {
mDatabase?.customColor = Color.parseColor(oldColor) database.customColor = oldColor.toColorInt()
oldColor oldColor
} }
dbCustomColorPref?.summary = defaultColorToShow dbCustomColorPref?.summary = defaultColorToShow
@@ -483,7 +536,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newCompression newCompression
} else { } else {
mDatabase?.compressionAlgorithm = oldCompression database.compressionAlgorithm = oldCompression
oldCompression oldCompression
} }
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources) dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
@@ -497,7 +550,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else { } else {
oldRecycleBin oldRecycleBin
} }
mDatabase?.setRecycleBin(recycleBinToShow) database.setRecycleBin(recycleBinToShow)
refreshRecycleBinGroup(database) refreshRecycleBinGroup(database)
} }
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
@@ -509,7 +562,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else { } else {
oldTemplatesGroup oldTemplatesGroup
} }
mDatabase?.setTemplatesGroup(templatesGroupToShow) database.setTemplatesGroup(templatesGroupToShow)
refreshTemplatesGroup(database) refreshTemplatesGroup(database)
} }
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
@@ -519,7 +572,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newMaxHistoryItems newMaxHistoryItems
} else { } else {
mDatabase?.historyMaxItems = oldMaxHistoryItems database.historyMaxItems = oldMaxHistoryItems
oldMaxHistoryItems oldMaxHistoryItems
} }
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString() dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
@@ -531,7 +584,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newMaxHistorySize newMaxHistorySize
} else { } else {
mDatabase?.historyMaxSize = oldMaxHistorySize database.historyMaxSize = oldMaxHistorySize
oldMaxHistorySize oldMaxHistorySize
} }
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString() dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
@@ -549,7 +602,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newEncryption newEncryption
} else { } else {
mDatabase?.encryptionAlgorithm = oldEncryption database.encryptionAlgorithm = oldEncryption
oldEncryption oldEncryption
} }
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString() mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
@@ -561,7 +614,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newKeyDerivationEngine newKeyDerivationEngine
} else { } else {
mDatabase?.kdfEngine = oldKeyDerivationEngine database.kdfEngine = oldKeyDerivationEngine
oldKeyDerivationEngine oldKeyDerivationEngine
} }
mKeyDerivationPref?.summary = kdfEngineToShow.toString() mKeyDerivationPref?.summary = kdfEngineToShow.toString()
@@ -578,7 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newIterations newIterations
} else { } else {
mDatabase?.numberKeyEncryptionRounds = oldIterations database.numberKeyEncryptionRounds = oldIterations
oldIterations oldIterations
} }
mRoundPref?.summary = roundsToShow.toString() mRoundPref?.summary = roundsToShow.toString()
@@ -590,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newMemoryUsage newMemoryUsage
} else { } else {
mDatabase?.memoryUsage = oldMemoryUsage database.memoryUsage = oldMemoryUsage
oldMemoryUsage oldMemoryUsage
} }
mMemoryPref?.summary = memoryToShow.toString() mMemoryPref?.summary = memoryToShow.toString()
@@ -602,7 +655,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newParallelism newParallelism
} else { } else {
mDatabase?.parallelism = oldParallelism database.parallelism = oldParallelism
oldParallelism oldParallelism
} }
mParallelismPref?.summary = parallelismToShow.toString() mParallelismPref?.summary = parallelismToShow.toString()

View File

@@ -108,7 +108,7 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.auto_focus_search_default)) context.resources.getBoolean(R.bool.auto_focus_search_default))
} }
fun searchSubdomains(context: Context): Boolean { fun searchSubDomains(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.subdomain_search_key), return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
context.resources.getBoolean(R.bool.subdomain_search_default)) context.resources.getBoolean(R.bool.subdomain_search_default))
@@ -352,6 +352,8 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.search_option_username_default)) context.resources.getBoolean(R.bool.search_option_username_default))
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key), searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
context.resources.getBoolean(R.bool.search_option_password_default)) context.resources.getBoolean(R.bool.search_option_password_default))
searchInAppIds = prefs.getBoolean(context.getString(R.string.search_option_application_id_key),
context.resources.getBoolean(R.bool.search_option_application_id_default))
searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key), searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
context.resources.getBoolean(R.bool.search_option_url_default)) context.resources.getBoolean(R.bool.search_option_url_default))
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key), searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
@@ -389,6 +391,8 @@ object PreferencesUtil {
searchParameters.searchInUsernames) searchParameters.searchInUsernames)
putBoolean(context.getString(R.string.search_option_password_key), putBoolean(context.getString(R.string.search_option_password_key),
searchParameters.searchInPasswords) searchParameters.searchInPasswords)
putBoolean(context.getString(R.string.search_option_application_id_key),
searchParameters.searchInAppIds)
putBoolean(context.getString(R.string.search_option_url_key), putBoolean(context.getString(R.string.search_option_url_key),
searchParameters.searchInUrls) searchParameters.searchInUrls)
putBoolean(context.getString(R.string.search_option_expired_key), putBoolean(context.getString(R.string.search_option_expired_key),
@@ -686,6 +690,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.keyboard_previous_lock_default)) context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
} }
fun isPasskeyCloseDatabaseEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_close_database_key),
context.resources.getBoolean(R.bool.passkeys_close_database_default))
}
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean { fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key), return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
@@ -854,6 +864,10 @@ object PreferencesUtil {
context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.passkeys_close_database_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.passkeys_auto_select_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.passkeys_backup_eligibility_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.passkeys_backup_state_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())

View File

@@ -70,8 +70,12 @@ open class SettingsActivity
// To apply navigation bar with background color // To apply navigation bar with background color
/* TODO Settings nav bar /* TODO Settings nav bar
setTransparentNavigationBar { setTransparentNavigationBar {
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP) coordinatorLayout?.applyWindowInsets(EnumSet.of(
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM) WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}*/ }*/
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
@@ -155,10 +159,6 @@ open class SettingsActivity
return coordinatorLayout return coordinatorLayout
} }
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
return false
}
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
database: ContextualDatabase, database: ContextualDatabase,
actionTask: String, actionTask: String,
@@ -188,7 +188,7 @@ open class SettingsActivity
} }
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) { override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
assignPassword(mainCredential) assignMainCredential(mainCredential)
} }
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {} override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}

View File

@@ -95,20 +95,16 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
return dialog return dialog
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) var initColor = database.customColor
if (initColor != null) {
database?.let { enableSwitchView.isChecked = true
var initColor = it.customColor } else {
if (initColor != null) { enableSwitchView.isChecked = false
enableSwitchView.isChecked = true initColor = DEFAULT_COLOR
} else {
enableSwitchView.isChecked = false
initColor = DEFAULT_COLOR
}
chromaColorView.currentColor = initColor
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
} }
chromaColorView.currentColor = initColor
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -50,16 +50,14 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
setExplanationText(R.string.database_data_compression_summary) setExplanationText(R.string.database_data_compression_summary)
mRecyclerView?.adapter = mCompressionAdapter mRecyclerView?.adapter = mCompressionAdapter
compressionSelected = database.compressionAlgorithm
database?.let { mCompressionAdapter?.setItems(
compressionSelected = it.compressionAlgorithm items = database.availableCompressionAlgorithms,
mCompressionAdapter?.setItems(it.availableCompressionAlgorithms, compressionSelected) itemUsed = compressionSelected
} )
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) inputText = database.defaultUsername
inputText = database?.defaultUsername?: ""
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) inputText = database.description
inputText = database?.description ?: ""
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -51,12 +51,9 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) algorithmSelected = database.encryptionAlgorithm
database?.let { mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
algorithmSelected = database.encryptionAlgorithm
mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
}
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -54,12 +54,12 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) kdfEngineSelected = database.kdfEngine
database?.let { mKdfAdapter?.setItems(
kdfEngineSelected = database.kdfEngine items = database.availableKdfEngines,
mKdfAdapter?.setItems(database.availableKdfEngines, kdfEngineSelected) itemUsed = kdfEngineSelected
} )
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -31,19 +31,17 @@ class DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePrefer
setExplanationText(R.string.max_history_items_summary) setExplanationText(R.string.max_history_items_summary)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) val maxItemsDatabase = database.historyMaxItems
database?.historyMaxItems?.let { maxItemsDatabase -> inputText = maxItemsDatabase.toString()
inputText = maxItemsDatabase.toString() setSwitchAction({ isChecked ->
setSwitchAction({ isChecked -> inputText = if (!isChecked) {
inputText = if (!isChecked) { NONE_MAX_HISTORY_ITEMS.toString()
NONE_MAX_HISTORY_ITEMS.toString() } else {
} else { DEFAULT_MAX_HISTORY_ITEMS.toString()
DEFAULT_MAX_HISTORY_ITEMS.toString() }
} showInputText(isChecked)
showInputText(isChecked) }, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
}, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
}
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -34,31 +34,29 @@ class DatabaseMaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePrefere
setExplanationText(R.string.max_history_size_summary) setExplanationText(R.string.max_history_size_summary)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) val maxItemsDatabase = database.historyMaxSize
database?.historyMaxSize?.let { maxItemsDatabase -> dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE) .toBetterByteFormat()
.toBetterByteFormat() inputText = dataByte.number.toString()
inputText = dataByte.number.toString() if (dataByte.number >= 0) {
if (dataByte.number >= 0) { setUnitText(dataByte.format.stringId)
setUnitText(dataByte.format.stringId) } else {
} else { unitText = null
unitText = null
}
setSwitchAction({ isChecked ->
if (!isChecked) {
dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
inputText = INFINITE_MAX_HISTORY_SIZE.toString()
unitText = null
} else {
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
showInputText(isChecked)
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
} }
setSwitchAction({ isChecked ->
if (!isChecked) {
dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
inputText = INFINITE_MAX_HISTORY_SIZE.toString()
unitText = null
} else {
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
showInputText(isChecked)
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -34,15 +34,12 @@ class DatabaseMemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreference
setExplanationText(R.string.memory_usage_explanation) setExplanationText(R.string.memory_usage_explanation)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) val memoryBytes = database.memoryUsage
database?.let { dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
val memoryBytes = database.memoryUsage .toBetterByteFormat()
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE) inputText = dataByte.number.toString()
.toBetterByteFormat() setUnitText(dataByte.format.stringId)
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) inputText = database.name
inputText = database?.name ?: ""
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -31,9 +31,8 @@ class DatabaseParallelismPreferenceDialogFragmentCompat : DatabaseSavePreference
setExplanationText(R.string.parallelism_explanation) setExplanationText(R.string.parallelism_explanation)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) inputText = database.parallelism.toString()
inputText = database?.parallelism?.toString() ?: MIN_PARALLELISM.toString()
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -48,12 +48,9 @@ class DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) mGroupRecycleBin = database.recycleBin
database?.let { mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
mGroupRecycleBin = database.recycleBin
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
}
} }
override fun onItemSelected(item: Group) { override fun onItemSelected(item: Group) {

View File

@@ -46,6 +46,8 @@ class DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat : DatabaseSavePre
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase) {}
companion object { companion object {
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat { fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {

View File

@@ -32,9 +32,8 @@ class DatabaseRoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
explanationText = getString(R.string.rounds_explanation) explanationText = getString(R.string.rounds_explanation)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) inputText = database.numberKeyEncryptionRounds.toString()
inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString()
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -22,6 +22,9 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.androidclearchroma.ChromaUtil import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
@@ -32,13 +35,15 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseSavePreferenceDialogFragmentCompat abstract class DatabaseSavePreferenceDialogFragmentCompat
: InputPreferenceDialogFragmentCompat(), DatabaseRetrieval { : InputPreferenceDialogFragmentCompat(), DatabaseRetrieval {
private var mDatabaseAutoSaveEnable = true private var mDatabaseAutoSaveEnable = true
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: ContextualDatabase? = null protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@@ -47,18 +52,32 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mDatabaseViewModel.database.observe(this) { database -> lifecycleScope.launch {
onDatabaseRetrieved(database) repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
} }
}
override fun onResume() {
super.onResume()
onDatabaseRetrieved(mDatabase)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
this.mDatabase = database
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -77,8 +96,10 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
// To inherit to save element in database // To inherit to save element in database
} }
protected fun saveColor(oldColor: Int?, protected fun saveColor(
newColor: Int?) { oldColor: Int?,
newColor: Int?
) {
val oldColorString = if (oldColor != null) val oldColorString = if (oldColor != null)
ChromaUtil.getFormattedColorString(oldColor, false) ChromaUtil.getFormattedColorString(oldColor, false)
else else
@@ -87,77 +108,158 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
ChromaUtil.getFormattedColorString(newColor, false) ChromaUtil.getFormattedColorString(newColor, false)
else else
"" ""
mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable) mDatabaseViewModel.saveColor(
oldColorString,
newColorString,
mDatabaseAutoSaveEnable
)
} }
protected fun saveCompression(oldCompression: CompressionAlgorithm, protected fun saveCompression(
newCompression: CompressionAlgorithm oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm
) { ) {
mDatabaseViewModel.saveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable) mDatabaseViewModel.saveCompression(
oldCompression,
newCompression,
mDatabaseAutoSaveEnable
)
} }
protected fun saveDefaultUsername(oldUsername: String, protected fun saveDefaultUsername(
newUsername: String) { oldUsername: String,
mDatabaseViewModel.saveDefaultUsername(oldUsername, newUsername, mDatabaseAutoSaveEnable) newUsername: String
) {
mDatabaseViewModel.saveDefaultUsername(
oldUsername,
newUsername,
mDatabaseAutoSaveEnable
)
} }
protected fun saveDescription(oldDescription: String, protected fun saveDescription(
newDescription: String) { oldDescription: String,
mDatabaseViewModel.saveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable) newDescription: String
) {
mDatabaseViewModel.saveDescription(
oldDescription,
newDescription,
mDatabaseAutoSaveEnable
)
} }
protected fun saveEncryption(oldEncryption: EncryptionAlgorithm, protected fun saveEncryption(
newEncryptionAlgorithm: EncryptionAlgorithm) { oldEncryption: EncryptionAlgorithm,
mDatabaseViewModel.saveEncryption(oldEncryption, newEncryptionAlgorithm, mDatabaseAutoSaveEnable) newEncryptionAlgorithm: EncryptionAlgorithm
) {
mDatabaseViewModel.saveEncryption(
oldEncryption,
newEncryptionAlgorithm,
mDatabaseAutoSaveEnable
)
} }
protected fun saveKeyDerivation(oldKeyDerivation: KdfEngine, protected fun saveKeyDerivation(
newKeyDerivation: KdfEngine) { oldKeyDerivation: KdfEngine,
mDatabaseViewModel.saveKeyDerivation(oldKeyDerivation, newKeyDerivation, mDatabaseAutoSaveEnable) newKeyDerivation: KdfEngine
) {
mDatabaseViewModel.saveKeyDerivation(
oldKeyDerivation,
newKeyDerivation,
mDatabaseAutoSaveEnable
)
} }
protected fun saveName(oldName: String, protected fun saveName(
newName: String) { oldName: String,
mDatabaseViewModel.saveName(oldName, newName, mDatabaseAutoSaveEnable) newName: String
) {
mDatabaseViewModel.saveName(
oldName,
newName,
mDatabaseAutoSaveEnable
)
} }
protected fun saveRecycleBin(oldGroup: Group?, protected fun saveRecycleBin(
newGroup: Group?) { oldGroup: Group?,
mDatabaseViewModel.saveRecycleBin(oldGroup, newGroup, mDatabaseAutoSaveEnable) newGroup: Group?
) {
mDatabaseViewModel.saveRecycleBin(
oldGroup,
newGroup,
mDatabaseAutoSaveEnable
)
} }
protected fun removeUnlinkedData() { protected fun removeUnlinkedData() {
mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable) mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable)
} }
protected fun saveTemplatesGroup(oldGroup: Group?, protected fun saveTemplatesGroup(
newGroup: Group?) { oldGroup: Group?,
mDatabaseViewModel.saveTemplatesGroup(oldGroup, newGroup, mDatabaseAutoSaveEnable) newGroup: Group?
) {
mDatabaseViewModel.saveTemplatesGroup(
oldGroup,
newGroup,
mDatabaseAutoSaveEnable
)
} }
protected fun saveMaxHistoryItems(oldNumber: Int, protected fun saveMaxHistoryItems(
newNumber: Int) { oldNumber: Int,
mDatabaseViewModel.saveMaxHistoryItems(oldNumber, newNumber, mDatabaseAutoSaveEnable) newNumber: Int
) {
mDatabaseViewModel.saveMaxHistoryItems(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
} }
protected fun saveMaxHistorySize(oldNumber: Long, protected fun saveMaxHistorySize(
newNumber: Long) { oldNumber: Long,
mDatabaseViewModel.saveMaxHistorySize(oldNumber, newNumber, mDatabaseAutoSaveEnable) newNumber: Long
) {
mDatabaseViewModel.saveMaxHistorySize(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
} }
protected fun saveMemoryUsage(oldNumber: Long, protected fun saveMemoryUsage(
newNumber: Long) { oldNumber: Long,
mDatabaseViewModel.saveMemoryUsage(oldNumber, newNumber, mDatabaseAutoSaveEnable) newNumber: Long
) {
mDatabaseViewModel.saveMemoryUsage(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
} }
protected fun saveParallelism(oldNumber: Long, protected fun saveParallelism(
newNumber: Long) { oldNumber: Long,
mDatabaseViewModel.saveParallelism(oldNumber, newNumber, mDatabaseAutoSaveEnable) newNumber: Long
) {
mDatabaseViewModel.saveParallelism(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
} }
protected fun saveIterations(oldNumber: Long, protected fun saveIterations(
newNumber: Long) { oldNumber: Long,
mDatabaseViewModel.saveIterations(oldNumber, newNumber, mDatabaseAutoSaveEnable) newNumber: Long
) {
mDatabaseViewModel.saveIterations(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
} }
companion object { companion object {

View File

@@ -48,12 +48,9 @@ class DatabaseTemplatesGroupPreferenceDialogFragmentCompat
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) mGroupTemplates = database.templatesGroup
database?.let { mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
mGroupTemplates = database.templatesGroup
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
}
} }
override fun onItemSelected(item: Group) { override fun onItemSelected(item: Group) {

View File

@@ -13,15 +13,13 @@ import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
object AppUtil { object AppUtil {
fun randomRequestCode(): Int {
return (Math.random() * Integer.MAX_VALUE).toInt()
}
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean { fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
try { try {
this.applicationContext.packageManager.getPackageInfoCompat( this.applicationContext.packageManager.getPackageInfoCompat(
@@ -79,29 +77,6 @@ object AppUtil {
) )
} }
/**
* Get the concrete web domain AKA without sub domain if needed
*/
fun getConcreteWebDomain(context: Context,
webDomain: String?,
concreteWebDomain: (String?) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
if (webDomain != null) {
// Warning, web domain can contains IP, don't crop in this case
if (PreferencesUtil.searchSubdomains(context)
|| Regex(SearchInfo.WEB_IP_REGEX).matches(webDomain)) {
concreteWebDomain.invoke(webDomain)
} else {
val publicSuffixList = PublicSuffixList(context)
concreteWebDomain.invoke(publicSuffixList
.getPublicSuffixPlusOne(webDomain).await())
}
} else {
concreteWebDomain.invoke(null)
}
}
}
@RequiresApi(Build.VERSION_CODES.P) @RequiresApi(Build.VERSION_CODES.P)
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> { fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
val packageManager = context.packageManager val packageManager = context.packageManager

View File

@@ -19,30 +19,39 @@
*/ */
package com.kunzisoft.keepass.utils package com.kunzisoft.keepass.utils
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.EmptyCoroutineContext
/** /**
* Class to invoke action in a separate IO thread * Class to invoke action in a separate IO thread
*/ */
class IOActionTask<T>( class IOActionTask<T>(
private val action: () -> T , private val action: () -> T,
private val afterActionListener: ((T?) -> Unit)? = null) { private val onActionComplete: ((T?) -> Unit)? = null,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
private val mainScope = CoroutineScope(Dispatchers.Main) private val exceptionHandler: CoroutineExceptionHandler? = null
) {
fun execute() { fun execute() {
mainScope.launch { scope.launch(exceptionHandler ?: EmptyCoroutineContext) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val asyncResult: Deferred<T?> = async { val asyncResult: Deferred<T?> = async {
try { exceptionHandler?.let {
action.invoke() action.invoke()
} catch (e: Exception) { } ?: try {
e.printStackTrace() action.invoke()
null } catch (e: Exception) {
} e.printStackTrace()
null
}
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
afterActionListener?.invoke(asyncResult.await()) onActionComplete?.invoke(asyncResult.await())
} }
} }
} }

View File

@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.ImageView import android.widget.ImageView
@@ -30,8 +29,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
private var searchTitle: CompoundButton private var searchTitle: CompoundButton
private var searchUsername: CompoundButton private var searchUsername: CompoundButton
private var searchPassword: CompoundButton private var searchPassword: CompoundButton
private var searchApplicationId: CompoundButton
private var searchURL: CompoundButton private var searchURL: CompoundButton
private var searchByURLDomain: Boolean = false private var searchByURLDomain: Boolean = false
private var searchByURLSubDomain: Boolean = false
private var searchExpired: CompoundButton private var searchExpired: CompoundButton
private var searchNotes: CompoundButton private var searchNotes: CompoundButton
private var searchOther: CompoundButton private var searchOther: CompoundButton
@@ -50,8 +51,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
this.searchInTitles = searchTitle.isChecked this.searchInTitles = searchTitle.isChecked
this.searchInUsernames = searchUsername.isChecked this.searchInUsernames = searchUsername.isChecked
this.searchInPasswords = searchPassword.isChecked this.searchInPasswords = searchPassword.isChecked
this.searchInAppIds = searchApplicationId.isChecked
this.searchInUrls = searchURL.isChecked this.searchInUrls = searchURL.isChecked
this.searchByDomain = searchByURLDomain this.searchByDomain = searchByURLDomain
this.searchBySubDomain = searchByURLSubDomain
this.searchInExpired = searchExpired.isChecked this.searchInExpired = searchExpired.isChecked
this.searchInNotes = searchNotes.isChecked this.searchInNotes = searchNotes.isChecked
this.searchInOther = searchOther.isChecked this.searchInOther = searchOther.isChecked
@@ -71,8 +74,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchTitle.isChecked = value.searchInTitles searchTitle.isChecked = value.searchInTitles
searchUsername.isChecked = value.searchInUsernames searchUsername.isChecked = value.searchInUsernames
searchPassword.isChecked = value.searchInPasswords searchPassword.isChecked = value.searchInPasswords
searchApplicationId.isChecked = value.searchInAppIds
searchURL.isChecked = value.searchInUrls searchURL.isChecked = value.searchInUrls
searchByURLDomain = value.searchByDomain searchByURLDomain = value.searchByDomain
searchByURLSubDomain = value.searchBySubDomain
searchExpired.isChecked = value.searchInExpired searchExpired.isChecked = value.searchInExpired
searchNotes.isChecked = value.searchInNotes searchNotes.isChecked = value.searchInNotes
searchOther.isChecked = value.searchInOther searchOther.isChecked = value.searchInOther
@@ -87,7 +92,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = { private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
// To recalculate height // To recalculate height
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) { if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
searchAdvanceFiltersContainer?.expand( searchAdvanceFiltersContainer?.expand(
false, false,
searchAdvanceFiltersContainer?.getFullHeight() searchAdvanceFiltersContainer?.getFullHeight()
@@ -110,6 +115,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchTitle = findViewById(R.id.search_chip_title) searchTitle = findViewById(R.id.search_chip_title)
searchUsername = findViewById(R.id.search_chip_username) searchUsername = findViewById(R.id.search_chip_username)
searchPassword = findViewById(R.id.search_chip_password) searchPassword = findViewById(R.id.search_chip_password)
searchApplicationId = findViewById(R.id.search_chip_application_id)
searchURL = findViewById(R.id.search_chip_url) searchURL = findViewById(R.id.search_chip_url)
searchExpired = findViewById(R.id.search_chip_expires) searchExpired = findViewById(R.id.search_chip_expires)
searchNotes = findViewById(R.id.search_chip_note) searchNotes = findViewById(R.id.search_chip_note)
@@ -125,7 +131,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
// Expand menu with button // Expand menu with button
searchExpandButton.setOnClickListener { searchExpandButton.setOnClickListener {
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE val isVisible = searchAdvanceFiltersContainer?.visibility == VISIBLE
if (isVisible) if (isVisible)
closeAdvancedFilters() closeAdvancedFilters()
else else
@@ -156,6 +162,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchParameters.searchInPasswords = isChecked searchParameters.searchInPasswords = isChecked
mOnParametersChangeListener?.invoke(searchParameters) mOnParametersChangeListener?.invoke(searchParameters)
} }
searchApplicationId.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInAppIds = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchURL.setOnCheckedChangeListener { _, isChecked -> searchURL.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInUrls = isChecked searchParameters.searchInUrls = isChecked
mOnParametersChangeListener?.invoke(searchParameters) mOnParametersChangeListener?.invoke(searchParameters)
@@ -200,10 +210,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers) searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
} }
fun setCurrentGroupText(text: String) { fun setCurrentGroupText(text: String?) {
val maxChars = 12 val maxChars = 12
searchCurrentGroup.text = when { searchCurrentGroup.text = when {
text.isEmpty() -> context.getString(R.string.current_group) text.isNullOrEmpty() -> context.getString(R.string.current_group)
text.length > maxChars -> text.substring(0, maxChars) + "" text.length > maxChars -> text.substring(0, maxChars) + ""
else -> text else -> text
} }
@@ -213,6 +223,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchOther.isVisible = available searchOther.isVisible = available
} }
fun availableApplicationIds(available: Boolean) {
searchApplicationId.isVisible = available
}
fun availableTags(available: Boolean) { fun availableTags(available: Boolean) {
searchTag.isVisible = available searchTag.isVisible = available
} }
@@ -243,16 +257,20 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
) )
} }
fun showSearchExpandButton(show: Boolean) {
searchExpandButton.isVisible = show
}
override fun setVisibility(visibility: Int) { override fun setVisibility(visibility: Int) {
when (visibility) { when (visibility) {
View.VISIBLE -> { VISIBLE -> {
searchAdvanceFiltersContainer?.visibility = View.GONE searchAdvanceFiltersContainer?.visibility = GONE
searchContainer.showByFading() searchContainer.showByFading()
} }
else -> { else -> {
searchContainer.hideByFading() searchContainer.hideByFading()
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) { if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
searchAdvanceFiltersContainer?.visibility = View.INVISIBLE searchAdvanceFiltersContainer?.visibility = INVISIBLE
searchAdvanceFiltersContainer?.collapse() searchAdvanceFiltersContainer?.collapse()
} }
} }

View File

@@ -24,7 +24,6 @@ import android.animation.AnimatorSet
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.PorterDuff import android.graphics.PorterDuff
@@ -66,6 +65,7 @@ import com.kunzisoft.keepass.database.exception.LocalizedException
import com.kunzisoft.keepass.database.helper.getLocalizedMessage import com.kunzisoft.keepass.database.helper.getLocalizedMessage
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import java.util.EnumSet
/** /**
@@ -317,9 +317,7 @@ fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) { fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
// Only in portrait if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector) window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
if (applyToStatusBar) { if (applyToStatusBar) {
@@ -335,7 +333,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
/** /**
* Apply a margin to a view to fix the window inset * Apply a margin to a view to fix the window inset
*/ */
fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.BOTTOM) { fun View.applyWindowInsets(positions: EnumSet<WindowInsetPosition>) {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets -> ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
var consumed = false var consumed = false
@@ -351,52 +349,78 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
} }
} }
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()
when (position) { or WindowInsetsCompat.Type.displayCutout()
WindowInsetPosition.TOP -> { or WindowInsetsCompat.Type.ime())
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> { val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
topMargin = insets.top
val wantTopMargins = positions.contains(WindowInsetPosition.TOP_MARGINS)
val wantBottomMargins = positions.contains(WindowInsetPosition.BOTTOM_MARGINS)
val wantStartMargins = positions.contains(WindowInsetPosition.START_MARGINS)
val wantEndMargins = positions.contains(WindowInsetPosition.END_MARGINS)
if (view.layoutParams is ViewGroup.MarginLayoutParams
&& (wantTopMargins || wantBottomMargins || wantStartMargins || wantEndMargins)) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
if (wantTopMargins) {
topMargin = insets.top
}
if (wantBottomMargins) {
bottomMargin = insets.bottom
}
if (wantStartMargins) {
if (isRtl) {
rightMargin = insets.right
} else {
leftMargin = insets.left
} }
} }
} if (wantEndMargins) {
WindowInsetPosition.LEGIT_TOP -> { if (isRtl) {
if (view.layoutParams is ViewGroup.MarginLayoutParams) { leftMargin = insets.left
view.updateLayoutParams<ViewGroup.MarginLayoutParams> { } else {
topMargin = 0 rightMargin = insets.right
}
}
}
WindowInsetPosition.BOTTOM -> {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
}
}
}
WindowInsetPosition.BOTTOM_IME -> {
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = if (imeHeight > 1) 0 else insets.bottom
}
}
}
WindowInsetPosition.TOP_BOTTOM_IME -> {
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
bottomMargin = if (imeHeight > 1) imeHeight else 0
} }
} }
} }
} }
val wantTopPadding = positions.contains(WindowInsetPosition.TOP_PADDING)
val wantBottomPadding = positions.contains(WindowInsetPosition.BOTTOM_PADDING)
val wantStartPadding = positions.contains(WindowInsetPosition.START_PADDING)
val wantEndPadding = positions.contains(WindowInsetPosition.END_PADDING)
if (wantTopPadding || wantBottomPadding || wantStartPadding || wantEndPadding) {
val topPadding = if (wantTopPadding) insets.top else 0
val bottomPadding = if (wantBottomPadding) insets.bottom else 0
var leftPadding = 0
var rightPadding = 0
if (wantStartPadding) {
if (isRtl) {
rightPadding = insets.right
} else {
leftPadding = insets.left
}
}
if (wantEndPadding) {
if (isRtl) {
leftPadding = insets.left
} else {
rightPadding = insets.right
}
}
setPadding(leftPadding, topPadding, rightPadding, bottomPadding)
}
// If any of the children consumed the insets, return an appropriate value // If any of the children consumed the insets, return an appropriate value
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
} }
} }
enum class WindowInsetPosition { enum class WindowInsetPosition {
TOP, BOTTOM, LEGIT_TOP, BOTTOM_IME, TOP_BOTTOM_IME TOP_MARGINS, BOTTOM_MARGINS, START_MARGINS, END_MARGINS,
TOP_PADDING, BOTTOM_PADDING, START_PADDING, END_PADDING,
} }

View File

@@ -1,214 +1,500 @@
package com.kunzisoft.keepass.viewmodels package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.LiveData import android.app.Application
import androidx.lifecycle.MutableLiveData import android.net.Uri
import androidx.lifecycle.ViewModel import android.os.Bundle
import androidx.lifecycle.AndroidViewModel
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.ProgressMessage
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
class DatabaseViewModel: ViewModel() { class DatabaseViewModel(application: Application): AndroidViewModel(application) {
val database : LiveData<ContextualDatabase?> get() = _database private val mDatabaseState = MutableStateFlow<ContextualDatabase?>(null)
private val _database = MutableLiveData<ContextualDatabase?>() val databaseState: StateFlow<ContextualDatabase?> = mDatabaseState
val actionFinished : LiveData<ActionResult> get() = _actionFinished val database: ContextualDatabase?
private val _actionFinished = SingleLiveEvent<ActionResult>() get() = databaseState.value
val saveDatabase : LiveData<Boolean> get() = _saveDatabase private val mActionState = MutableStateFlow<ActionState>(ActionState.Loading)
private val _saveDatabase = SingleLiveEvent<Boolean>() val actionState: StateFlow<ActionState> = mActionState
val mergeDatabase : LiveData<Boolean> get() = _mergeDatabase private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider(
private val _mergeDatabase = SingleLiveEvent<Boolean>() context = application
)
val reloadDatabase : LiveData<Boolean> get() = _reloadDatabase init {
private val _reloadDatabase = SingleLiveEvent<Boolean>() mDatabaseTaskProvider.onDatabaseRetrieved = { databaseRetrieved ->
val databaseWasReloaded = databaseRetrieved?.wasReloaded == true
if (databaseWasReloaded) {
mActionState.value = ActionState.OnDatabaseReloaded
}
if (database == null || database != databaseRetrieved || databaseWasReloaded) {
databaseRetrieved?.wasReloaded = false
mDatabaseState.value = databaseRetrieved
}
}
mDatabaseTaskProvider.onStartActionRequested = { bundle, actionTask ->
mActionState.value = ActionState.OnDatabaseActionRequested(bundle, actionTask)
}
mDatabaseTaskProvider.databaseInfoListener = object : DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(
previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo,
readOnlyDatabase: Boolean
) {
mActionState.value = ActionState.OnDatabaseInfoChanged(
previousDatabaseInfo,
newDatabaseInfo,
readOnlyDatabase
)
}
}
mDatabaseTaskProvider.actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
override fun onActionStarted(
database: ContextualDatabase,
progressMessage: ProgressMessage
) {
mActionState.value = ActionState.OnDatabaseActionStarted(database, progressMessage)
}
val saveName : LiveData<SuperString> get() = _saveName override fun onActionUpdated(
private val _saveName = SingleLiveEvent<SuperString>() database: ContextualDatabase,
progressMessage: ProgressMessage
) {
mActionState.value = ActionState.OnDatabaseActionUpdated(database, progressMessage)
}
val saveDescription : LiveData<SuperString> get() = _saveDescription override fun onActionStopped(database: ContextualDatabase?) {
private val _saveDescription = SingleLiveEvent<SuperString>() mActionState.value = ActionState.OnDatabaseActionStopped(database)
}
val saveDefaultUsername : LiveData<SuperString> get() = _saveDefaultUsername override fun onActionFinished(
private val _saveDefaultUsername = SingleLiveEvent<SuperString>() database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
mActionState.value = ActionState.OnDatabaseActionFinished(database, actionTask, result)
}
}
val saveColor : LiveData<SuperString> get() = _saveColor mDatabaseTaskProvider.registerProgressTask()
private val _saveColor = SingleLiveEvent<SuperString>()
val saveCompression : LiveData<SuperCompression> get() = _saveCompression
private val _saveCompression = SingleLiveEvent<SuperCompression>()
val removeUnlinkData : LiveData<Boolean> get() = _removeUnlinkData
private val _removeUnlinkData = SingleLiveEvent<Boolean>()
val saveRecycleBin : LiveData<SuperGroup> get() = _saveRecycleBin
private val _saveRecycleBin = SingleLiveEvent<SuperGroup>()
val saveTemplatesGroup : LiveData<SuperGroup> get() = _saveTemplatesGroup
private val _saveTemplatesGroup = SingleLiveEvent<SuperGroup>()
val saveMaxHistoryItems : LiveData<SuperInt> get() = _saveMaxHistoryItems
private val _saveMaxHistoryItems = SingleLiveEvent<SuperInt>()
val saveMaxHistorySize : LiveData<SuperLong> get() = _saveMaxHistorySize
private val _saveMaxHistorySize = SingleLiveEvent<SuperLong>()
val saveEncryption : LiveData<SuperEncryption> get() = _saveEncryption
private val _saveEncryption = SingleLiveEvent<SuperEncryption>()
val saveKeyDerivation : LiveData<SuperKeyDerivation> get() = _saveKeyDerivation
private val _saveKeyDerivation = SingleLiveEvent<SuperKeyDerivation>()
val saveIterations : LiveData<SuperLong> get() = _saveIterations
private val _saveIterations = SingleLiveEvent<SuperLong>()
val saveMemoryUsage : LiveData<SuperLong> get() = _saveMemoryUsage
private val _saveMemoryUsage = SingleLiveEvent<SuperLong>()
val saveParallelism : LiveData<SuperLong> get() = _saveParallelism
private val _saveParallelism = SingleLiveEvent<SuperLong>()
fun defineDatabase(database: ContextualDatabase?) {
this._database.value = database
} }
fun onActionFinished(database: ContextualDatabase, /*
actionTask: String, * Main database actions
result: ActionRunnable.Result) { */
this._actionFinished.value = ActionResult(database, actionTask, result)
fun loadDatabase(
databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
mDatabaseTaskProvider.startDatabaseLoad(
databaseUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
fixDuplicateUuid
)
} }
fun saveDatabase(save: Boolean) { fun createDatabase(
_saveDatabase.value = save databaseUri: Uri,
mainCredential: MainCredential
) {
mDatabaseTaskProvider.startDatabaseCreate(databaseUri, mainCredential)
} }
fun mergeDatabase(save: Boolean) { fun assignMainCredential(
_mergeDatabase.value = save databaseUri: Uri?,
mainCredential: MainCredential
) {
if (databaseUri != null) {
mDatabaseTaskProvider.startDatabaseAssignCredential(databaseUri, mainCredential)
}
}
fun saveDatabase(save: Boolean, saveToUri: Uri? = null) {
mDatabaseTaskProvider.startDatabaseSave(save, saveToUri)
}
fun mergeDatabase(
save: Boolean,
fromDatabaseUri: Uri? = null,
mainCredential: MainCredential? = null
) {
mDatabaseTaskProvider.startDatabaseMerge(save, fromDatabaseUri, mainCredential)
} }
fun reloadDatabase(fixDuplicateUuid: Boolean) { fun reloadDatabase(fixDuplicateUuid: Boolean) {
_reloadDatabase.value = fixDuplicateUuid mDatabaseTaskProvider.askToStartDatabaseReload(
conditionToAsk = database?.dataModifiedSinceLastLoading != false
) {
mDatabaseTaskProvider.startDatabaseReload(fixDuplicateUuid)
}
} }
fun saveName(oldValue: String, fun onDatabaseChangeValidated() {
newValue: String, mDatabaseTaskProvider.onDatabaseChangeValidated()
save: Boolean) {
_saveName.value = SuperString(oldValue, newValue, save)
} }
fun saveDescription(oldValue: String, /*
newValue: String, * Nodes actions
save: Boolean) { */
_saveDescription.value = SuperString(oldValue, newValue, save)
fun createEntry(
newEntry: Entry,
parent: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseCreateEntry(
newEntry,
parent,
save
)
} }
fun saveDefaultUsername(oldValue: String, fun updateEntry(
newValue: String, oldEntry: Entry,
save: Boolean) { entryToUpdate: Entry,
_saveDefaultUsername.value = SuperString(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseUpdateEntry(
oldEntry,
entryToUpdate,
save
)
} }
fun saveColor(oldValue: String, fun restoreEntryHistory(
newValue: String, mainEntryId: NodeId<UUID>,
save: Boolean) { entryHistoryPosition: Int,
_saveColor.value = SuperString(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseRestoreEntryHistory(
mainEntryId,
entryHistoryPosition,
save
)
} }
fun saveCompression(oldValue: CompressionAlgorithm, fun deleteEntryHistory(
newValue: CompressionAlgorithm, mainEntryId: NodeId<UUID>,
save: Boolean) { entryHistoryPosition: Int,
_saveCompression.value = SuperCompression(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseDeleteEntryHistory(
mainEntryId,
entryHistoryPosition,
save
)
}
fun createGroup(
newGroup: Group,
parent: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseCreateGroup(
newGroup,
parent,
save
)
}
fun updateGroup(
oldGroup: Group,
groupToUpdate: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseUpdateGroup(
oldGroup,
groupToUpdate,
save
)
}
fun copyNodes(
nodesToCopy: List<Node>,
newParent: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseCopyNodes(
nodesToCopy,
newParent,
save
)
}
fun moveNodes(
nodesToMove: List<Node>,
newParent: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseMoveNodes(
nodesToMove,
newParent,
save
)
}
fun deleteNodes(
nodes: List<Node>,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseDeleteNodes(
nodes,
save
)
}
/*
* Attributes
*/
fun buildNewAttachment(): BinaryData? {
return database?.buildNewBinaryAttachment()
}
/*
* Settings actions
*/
fun saveName(
oldValue: String,
newValue: String,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveName(
oldValue,
newValue,
save
)
}
fun saveDescription(
oldValue: String,
newValue: String,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveDescription(
oldValue,
newValue,
save
)
}
fun saveDefaultUsername(
oldValue: String,
newValue: String,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveDefaultUsername(
oldValue,
newValue,
save
)
}
fun saveColor(
oldValue: String,
newValue: String,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveColor(
oldValue,
newValue,
save
)
}
fun saveCompression(
oldValue: CompressionAlgorithm,
newValue: CompressionAlgorithm,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveCompression(
oldValue,
newValue,
save
)
} }
fun removeUnlinkedData(save: Boolean) { fun removeUnlinkedData(save: Boolean) {
_removeUnlinkData.value = save mDatabaseTaskProvider.startDatabaseRemoveUnlinkedData(save)
} }
fun saveRecycleBin(oldValue: Group?, fun saveRecycleBin(
newValue: Group?, oldValue: Group?,
save: Boolean) { newValue: Group?,
_saveRecycleBin.value = SuperGroup(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveRecycleBin(
oldValue,
newValue,
save
)
} }
fun saveTemplatesGroup(oldValue: Group?, fun saveTemplatesGroup(
newValue: Group?, oldValue: Group?,
save: Boolean) { newValue: Group?,
_saveTemplatesGroup.value = SuperGroup(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveTemplatesGroup(
oldValue,
newValue,
save
)
} }
fun saveMaxHistoryItems(oldValue: Int, fun saveMaxHistoryItems(
newValue: Int, oldValue: Int,
save: Boolean) { newValue: Int,
_saveMaxHistoryItems.value = SuperInt(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMaxHistoryItems(
oldValue,
newValue,
save
)
} }
fun saveMaxHistorySize(oldValue: Long, fun saveMaxHistorySize(
newValue: Long, oldValue: Long,
save: Boolean) { newValue: Long,
_saveMaxHistorySize.value = SuperLong(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMaxHistorySize(
oldValue,
newValue,
save
)
} }
fun saveEncryption(oldValue: EncryptionAlgorithm, fun saveEncryption(
newValue: EncryptionAlgorithm, oldValue: EncryptionAlgorithm,
save: Boolean) { newValue: EncryptionAlgorithm,
_saveEncryption.value = SuperEncryption(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveEncryption(
oldValue,
newValue,
save
)
} }
fun saveKeyDerivation(oldValue: KdfEngine, fun saveKeyDerivation(
newValue: KdfEngine, oldValue: KdfEngine,
save: Boolean) { newValue: KdfEngine,
_saveKeyDerivation.value = SuperKeyDerivation(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveKeyDerivation(
oldValue,
newValue,
save
)
} }
fun saveIterations(oldValue: Long, fun saveIterations(
newValue: Long, oldValue: Long,
save: Boolean) { newValue: Long,
_saveIterations.value = SuperLong(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveIterations(
oldValue,
newValue,
save
)
} }
fun saveMemoryUsage(oldValue: Long, fun saveMemoryUsage(
newValue: Long, oldValue: Long,
save: Boolean) { newValue: Long,
_saveMemoryUsage.value = SuperLong(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMemoryUsage(
oldValue,
newValue,
save
)
} }
fun saveParallelism(oldValue: Long, fun saveParallelism(
newValue: Long, oldValue: Long,
save: Boolean) { newValue: Long,
_saveParallelism.value = SuperLong(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveParallelism(
oldValue,
newValue,
save
)
} }
data class ActionResult(val database: ContextualDatabase, /*
val actionTask: String, * Hardware Key
val result: ActionRunnable.Result) */
data class SuperString(val oldValue: String,
val newValue: String,
val save: Boolean)
data class SuperInt(val oldValue: Int,
val newValue: Int,
val save: Boolean)
data class SuperLong(val oldValue: Long,
val newValue: Long,
val save: Boolean)
data class SuperMerge(val fixDuplicateUuid: Boolean,
val save: Boolean)
data class SuperCompression(val oldValue: CompressionAlgorithm,
val newValue: CompressionAlgorithm,
val save: Boolean)
data class SuperEncryption(val oldValue: EncryptionAlgorithm,
val newValue: EncryptionAlgorithm,
val save: Boolean)
data class SuperKeyDerivation(val oldValue: KdfEngine,
val newValue: KdfEngine,
val save: Boolean)
data class SuperGroup(val oldValue: Group?,
val newValue: Group?,
val save: Boolean)
fun onChallengeResponded(challengeResponse: ByteArray?) {
mDatabaseTaskProvider.startChallengeResponded(
challengeResponse ?: ByteArray(0)
)
}
override fun onCleared() {
super.onCleared()
mDatabaseTaskProvider.unregisterProgressTask()
mDatabaseTaskProvider.destroy()
}
sealed class ActionState {
object Loading: ActionState()
object OnDatabaseReloaded: ActionState()
data class OnDatabaseActionRequested(
val bundle: Bundle? = null,
val actionTask: String
): ActionState()
data class OnDatabaseInfoChanged(
val previousDatabaseInfo: SnapFileDatabaseInfo,
val newDatabaseInfo: SnapFileDatabaseInfo,
val readOnlyDatabase: Boolean
): ActionState()
data class OnDatabaseActionStarted(
var database: ContextualDatabase,
val progressMessage: ProgressMessage
): ActionState()
data class OnDatabaseActionUpdated(
var database: ContextualDatabase,
val progressMessage: ProgressMessage
): ActionState()
data class OnDatabaseActionStopped(
var database: ContextualDatabase?
): ActionState()
data class OnDatabaseActionFinished(
var database: ContextualDatabase,
val actionTask: String,
val result: ActionRunnable.Result
): ActionState()
}
} }

View File

@@ -3,6 +3,7 @@ package com.kunzisoft.keepass.viewmodels
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
@@ -16,10 +17,11 @@ import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.utils.IOActionTask import com.kunzisoft.keepass.utils.IOActionTask
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID import java.util.UUID
@@ -28,12 +30,18 @@ class EntryEditViewModel: NodeEditViewModel() {
private var mEntryId: NodeId<UUID>? = null private var mEntryId: NodeId<UUID>? = null
private var mParentId: NodeId<*>? = null private var mParentId: NodeId<*>? = null
private var mRegisterInfo: RegisterInfo? = null private var mRegisterInfo: RegisterInfo? = null
private var mSearchInfo: SearchInfo? = null
private var mParent: Group? = null private var mParent: Group? = null
private var mEntry: Entry? = null private var mEntry: Entry? = null
private var mIsTemplate: Boolean = false private var mIsTemplate: Boolean = false
private val mTempAttachments = mutableListOf<EntryAttachmentState>() private val mTempAttachments = mutableListOf<EntryAttachmentState>()
// To show dialog only one time
var backPressedAlreadyApproved = false
var warningOverwriteDataAlreadyApproved = false
// Useful to not relaunch a current action
private var actionLocked: Boolean = false
val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
private val _templatesEntry = MutableLiveData<TemplatesEntry?>() private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
@@ -73,24 +81,28 @@ class EntryEditViewModel: NodeEditViewModel() {
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>() private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
fun loadDatabase(database: ContextualDatabase?) { private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo) val uiState: StateFlow<UIState> = mUiState
fun loadTemplateEntry(database: ContextualDatabase?) {
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
} }
fun loadTemplateEntry(database: ContextualDatabase?, fun loadTemplateEntry(
entryId: NodeId<UUID>?, database: ContextualDatabase?,
parentId: NodeId<*>?, entryId: NodeId<UUID>?,
registerInfo: RegisterInfo?, parentId: NodeId<*>?,
searchInfo: SearchInfo?) { registerInfo: RegisterInfo?
) {
this.mEntryId = entryId this.mEntryId = entryId
this.mParentId = parentId this.mParentId = parentId
this.mRegisterInfo = registerInfo this.mRegisterInfo = registerInfo
this.mSearchInfo = searchInfo
database?.let { database?.let {
mEntryId?.let { mEntryId?.let {
IOActionTask( IOActionTask(
{ scope = viewModelScope,
action = {
// Create an Entry copy to modify from the database entry // Create an Entry copy to modify from the database entry
mEntry = database.getEntryById(it) mEntry = database.getEntryById(it)
// Retrieve the parent // Retrieve the parent
@@ -105,21 +117,24 @@ class EntryEditViewModel: NodeEditViewModel() {
database, database,
entry, entry,
mIsTemplate, mIsTemplate,
registerInfo, registerInfo
searchInfo
) )
} }
}, },
{ templatesEntry -> onActionComplete = { templatesEntry ->
mEntryId = null mEntryId = null
_templatesEntry.value = templatesEntry _templatesEntry.value = templatesEntry
if (templatesEntry?.overwrittenData == true) {
mUiState.value = UIState.ShowOverwriteMessage
}
} }
).execute() ).execute()
} }
mParentId?.let { mParentId?.let {
IOActionTask( IOActionTask(
{ scope = viewModelScope,
action = {
mParent = database.getGroupById(it) mParent = database.getGroupById(it)
mParent?.let { parentGroup -> mParent?.let { parentGroup ->
mEntry = database.createEntry()?.apply { mEntry = database.createEntry()?.apply {
@@ -145,12 +160,11 @@ class EntryEditViewModel: NodeEditViewModel() {
database, database,
mEntry, mEntry,
mIsTemplate, mIsTemplate,
registerInfo, registerInfo
searchInfo
) )
} }
}, },
{ templatesEntry -> onActionComplete = { templatesEntry ->
mParentId = null mParentId = null
_templatesEntry.value = templatesEntry _templatesEntry.value = templatesEntry
} }
@@ -159,33 +173,37 @@ class EntryEditViewModel: NodeEditViewModel() {
} }
} }
private fun decodeTemplateEntry(database: ContextualDatabase, private fun decodeTemplateEntry(
entry: Entry?, database: ContextualDatabase,
isTemplate: Boolean, entry: Entry?,
registerInfo: RegisterInfo?, isTemplate: Boolean,
searchInfo: SearchInfo?): TemplatesEntry { registerInfo: RegisterInfo?
): TemplatesEntry {
val templates = database.getTemplates(isTemplate) val templates = database.getTemplates(isTemplate)
val entryTemplate = entry?.let { database.getTemplate(it) } val entryTemplate = entry?.let { database.getTemplate(it) }
?: Template.STANDARD ?: Template.STANDARD
var entryInfo: EntryInfo? = null var entryInfo: EntryInfo? = null
var overwrittenData = false
// Decode the entry / load entry info // Decode the entry / load entry info
entry?.let { entry?.let {
database.decodeEntryWithTemplateConfiguration(it).let { entry -> database.decodeEntryWithTemplateConfiguration(it).let { entry ->
// Load entry info // Load entry info
entry.getEntryInfo(database, true).let { tempEntryInfo -> entry.getEntryInfo(database, true).let { tempEntryInfo ->
// Retrieve data from registration // Retrieve data from registration
// TODO only save registration
searchInfo?.let { tempSearchInfo ->
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
}
registerInfo?.let { regInfo -> registerInfo?.let { regInfo ->
tempEntryInfo.saveRegisterInfo(database, regInfo) overwrittenData = tempEntryInfo.saveRegisterInfo(database, regInfo)
} }
entryInfo = tempEntryInfo entryInfo = tempEntryInfo
} }
} }
} }
return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo) return TemplatesEntry(
isTemplate,
templates,
entryTemplate,
entryInfo,
overwrittenData
)
} }
fun changeTemplate(template: Template) { fun changeTemplate(template: Template) {
@@ -198,44 +216,52 @@ class EntryEditViewModel: NodeEditViewModel() {
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent) _requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
} }
fun unlockAction() {
actionLocked = false
}
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) { fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
IOActionTask( if (actionLocked.not()) {
{ actionLocked = true
removeTempAttachmentsNotCompleted(entryInfo) IOActionTask(
entry?.let { oldEntry -> scope = viewModelScope,
// Create a clone action = {
var newEntry = Entry(oldEntry) removeTempAttachmentsNotCompleted(entryInfo)
entry?.let { oldEntry ->
// Create a clone
var newEntry = Entry(oldEntry)
// Build info // Build info
newEntry.setEntryInfo(database, entryInfo) newEntry.setEntryInfo(database, entryInfo)
// Encode entry properties for template // Encode entry properties for template
_onTemplateChanged.value?.let { template -> _onTemplateChanged.value?.let { template ->
newEntry = newEntry =
database?.encodeEntryWithTemplateConfiguration(newEntry, template) database?.encodeEntryWithTemplateConfiguration(newEntry, template)
?: newEntry ?: newEntry
} }
// Delete temp attachment if not used // Delete temp attachment if not used
mTempAttachments.forEach { tempAttachmentState -> mTempAttachments.forEach { tempAttachmentState ->
val tempAttachment = tempAttachmentState.attachment val tempAttachment = tempAttachmentState.attachment
database?.attachmentPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) { if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
database.removeAttachmentIfNotUsed(tempAttachment) database.removeAttachmentIfNotUsed(tempAttachment)
}
} }
} }
}
// Return entry to save // Return entry to save
EntrySave(oldEntry, newEntry, parent) EntrySave(oldEntry, newEntry, parent)
}
},
onActionComplete = { entrySave ->
entrySave?.let {
_onEntrySaved.value = it
}
} }
}, ).execute()
{ entrySave -> }
entrySave?.let {
_onEntrySaved.value = it
}
}
).execute()
} }
private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) { private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) {
@@ -322,10 +348,13 @@ class EntryEditViewModel: NodeEditViewModel() {
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition) _onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
} }
data class TemplatesEntry(val isTemplate: Boolean, data class TemplatesEntry(
val templates: List<Template>, val isTemplate: Boolean,
val defaultTemplate: Template, val templates: List<Template>,
val entryInfo: EntryInfo?) val defaultTemplate: Template,
val entryInfo: EntryInfo?,
val overwrittenData: Boolean = false
)
data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?) data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?)
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?) data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
data class FieldEdition(val oldField: Field?, val newField: Field?) data class FieldEdition(val oldField: Field?, val newField: Field?)
@@ -333,6 +362,11 @@ class EntryEditViewModel: NodeEditViewModel() {
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment) data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float) data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
sealed class UIState {
object Loading: UIState()
object ShowOverwriteMessage: UIState()
}
companion object { companion object {
private val TAG = EntryEditViewModel::class.java.name private val TAG = EntryEditViewModel::class.java.name
} }

View File

@@ -20,6 +20,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_entry_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:filterTouchesWhenObscured="true"> android:filterTouchesWhenObscured="true">

View File

@@ -101,6 +101,13 @@
android:checked="false" android:checked="false"
style="@style/KeepassDXStyle.Chip.Filter" style="@style/KeepassDXStyle.Chip.Filter"
android:text="@string/entry_password"/> android:text="@string/entry_password"/>
<com.google.android.material.chip.Chip
android:id="@+id/search_chip_application_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
style="@style/KeepassDXStyle.Chip.Filter"
android:text="@string/entry_application_id"/>
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/search_chip_url" android:id="@+id/search_chip_url"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -452,7 +452,6 @@
<string name="menu_form_filling_settings">ملء النموذج</string> <string name="menu_form_filling_settings">ملء النموذج</string>
<string name="menu_reload_database">أعد تحميل البيانات</string> <string name="menu_reload_database">أعد تحميل البيانات</string>
<string name="menu_external_icon">أيقونة خارجية</string> <string name="menu_external_icon">أيقونة خارجية</string>
<string name="registration_mode">وضع التسجيل</string>
<string name="import_app_properties_title">استورد خصائص التطبيق</string> <string name="import_app_properties_title">استورد خصائص التطبيق</string>
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string> <string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string> <string name="export_app_properties_title">صدّر إعدادات التطبيق</string>

View File

@@ -485,7 +485,6 @@
<string name="search_mode">Axtarış modu</string> <string name="search_mode">Axtarış modu</string>
<string name="save_mode">Yadda saxlama modu</string> <string name="save_mode">Yadda saxlama modu</string>
<string name="selection_mode">Seçim modu</string> <string name="selection_mode">Seçim modu</string>
<string name="registration_mode">Qeydiyyat modu</string>
<string name="remember_database_locations_title">Məlumat bazalarının yerlərini xatırlayın</string> <string name="remember_database_locations_title">Məlumat bazalarının yerlərini xatırlayın</string>
<string name="remember_database_locations_summary">Məlumat bazalarının harada saxlanıldığını izlə</string> <string name="remember_database_locations_summary">Məlumat bazalarının harada saxlanıldığını izlə</string>
<string name="remember_hardware_key_summary">Aparat-təchizat açarlarının harada istifadə olunduğunu izlə</string> <string name="remember_hardware_key_summary">Aparat-təchizat açarlarının harada istifadə olunduğunu izlə</string>

View File

@@ -336,7 +336,6 @@
<string name="menu_keystore_remove_key">Izbrišite ključ za otključavanje uređaja</string> <string name="menu_keystore_remove_key">Izbrišite ključ za otključavanje uređaja</string>
<string name="subdomain_search_summary">Pretražujte veb domene sa ograničenjima poddomena</string> <string name="subdomain_search_summary">Pretražujte veb domene sa ograničenjima poddomena</string>
<string name="export_app_properties_title">Izvezite podešavanja aplikacije</string> <string name="export_app_properties_title">Izvezite podešavanja aplikacije</string>
<string name="registration_mode">Režim registracije</string>
<string name="remember_database_locations_title">Zapamtite lokacije baza podataka</string> <string name="remember_database_locations_title">Zapamtite lokacije baza podataka</string>
<string name="remember_hardware_key_title">Zapamtite hardverske ključeve</string> <string name="remember_hardware_key_title">Zapamtite hardverske ključeve</string>
<string name="remember_hardware_key_summary">Vodi evidenciju o korišćenim hardverskim ključevima</string> <string name="remember_hardware_key_summary">Vodi evidenciju o korišćenim hardverskim ključevima</string>

View File

@@ -296,7 +296,6 @@
<string name="search_mode">Рэжым пошуку</string> <string name="search_mode">Рэжым пошуку</string>
<string name="save_mode">Рэжым захавання</string> <string name="save_mode">Рэжым захавання</string>
<string name="selection_mode">Рэжым выбару</string> <string name="selection_mode">Рэжым выбару</string>
<string name="registration_mode">Рэжым рэгістрацыі</string>
<string name="remember_database_locations_title">Запамінаць размяшчэнне баз дадзеных</string> <string name="remember_database_locations_title">Запамінаць размяшчэнне баз дадзеных</string>
<string name="remember_database_locations_summary">Адсочвае, дзе захоўваюцца базы дадзеных</string> <string name="remember_database_locations_summary">Адсочвае, дзе захоўваюцца базы дадзеных</string>
<string name="remember_keyfile_locations_title">Запамінаць размяшчэнне файлаў ключоў</string> <string name="remember_keyfile_locations_title">Запамінаць размяшчэнне файлаў ключоў</string>

View File

@@ -462,7 +462,6 @@
<string name="error_invalid_OTP">Неприемлива тайна за OTP.</string> <string name="error_invalid_OTP">Неприемлива тайна за OTP.</string>
<string name="error_no_name">Въведете име.</string> <string name="error_no_name">Въведете име.</string>
<string name="hide_broken_locations_summary">Скрива вече несъществуващи хранилища от списъка с последно отваряните</string> <string name="hide_broken_locations_summary">Скрива вече несъществуващи хранилища от списъка с последно отваряните</string>
<string name="registration_mode">Режим регистрация</string>
<string name="remember_database_locations_title">Запомняне използваните хранилища</string> <string name="remember_database_locations_title">Запомняне използваните хранилища</string>
<string name="show_recent_files_title">Показване на последните хранилища</string> <string name="show_recent_files_title">Показване на последните хранилища</string>
<string name="search_mode">Режим търсене</string> <string name="search_mode">Режим търсене</string>

View File

@@ -209,7 +209,6 @@
<string name="search_mode">অনুসন্ধান মোড</string> <string name="search_mode">অনুসন্ধান মোড</string>
<string name="save_mode">সেভ মোড</string> <string name="save_mode">সেভ মোড</string>
<string name="selection_mode">নির্বাচন মোড</string> <string name="selection_mode">নির্বাচন মোড</string>
<string name="registration_mode">রেজিস্ট্রেশন মোড</string>
<string name="remember_keyfile_locations_summary">কী ফাইলগুলি কোথায় সংরক্ষণ করা হয় তা ট্র্যাক রাখে</string> <string name="remember_keyfile_locations_summary">কী ফাইলগুলি কোথায় সংরক্ষণ করা হয় তা ট্র্যাক রাখে</string>
<string name="show_recent_files_title">সাম্প্রতিক ফাইল দেখান</string> <string name="show_recent_files_title">সাম্প্রতিক ফাইল দেখান</string>
<string name="show_recent_files_summary">সাম্প্রতিক ডাটাবেসের অবস্থান দেখান</string> <string name="show_recent_files_summary">সাম্প্রতিক ডাটাবেসের অবস্থান দেখান</string>

View File

@@ -347,7 +347,6 @@
<string name="place_of_issue">Lloc d\'expedició</string> <string name="place_of_issue">Lloc d\'expedició</string>
<string name="style_brightness_summary">Escull tema clar o fosc</string> <string name="style_brightness_summary">Escull tema clar o fosc</string>
<string name="hardware_key">Clau física</string> <string name="hardware_key">Clau física</string>
<string name="registration_mode">Mode de registre</string>
<string name="ignore_chars_filter">Ignora caràcters</string> <string name="ignore_chars_filter">Ignora caràcters</string>
<string name="ask">Pregunta</string> <string name="ask">Pregunta</string>
<string name="searchable">Cercable</string> <string name="searchable">Cercable</string>

View File

@@ -480,7 +480,6 @@
<string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string> <string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string>
<string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string> <string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string>
<string name="warning_empty_recycle_bin">Trvale odstranit všechny uzly z koše\?</string> <string name="warning_empty_recycle_bin">Trvale odstranit všechny uzly z koše\?</string>
<string name="registration_mode">Registrace</string>
<string name="save_mode">Režim ukládání</string> <string name="save_mode">Režim ukládání</string>
<string name="search_mode">Vyhledávání</string> <string name="search_mode">Vyhledávání</string>
<string name="error_field_name_already_exists">Jméno kolonky již existuje.</string> <string name="error_field_name_already_exists">Jméno kolonky již existuje.</string>

View File

@@ -475,7 +475,6 @@
\n \n
\nDatabasen kan blive meget stor og reducere ydeevnen med denne overførelse.</string> \nDatabasen kan blive meget stor og reducere ydeevnen med denne overførelse.</string>
<string name="warning_empty_recycle_bin">Slet alle noder permanent fra papirkurven\?</string> <string name="warning_empty_recycle_bin">Slet alle noder permanent fra papirkurven\?</string>
<string name="registration_mode">Registreringstilstand</string>
<string name="save_mode">Gem-tilstand</string> <string name="save_mode">Gem-tilstand</string>
<string name="search_mode">Søgetilstand</string> <string name="search_mode">Søgetilstand</string>
<string name="error_registration_read_only">Det er ikke tilladt at gemme et nyt element i en skrivebeskyttet database.</string> <string name="error_registration_read_only">Det er ikke tilladt at gemme et nyt element i en skrivebeskyttet database.</string>

View File

@@ -486,7 +486,6 @@
<string name="notification">Benachrichtigung</string> <string name="notification">Benachrichtigung</string>
<string name="biometric_security_update_required">Biometrische Sicherheitsaktualisierung erforderlich.</string> <string name="biometric_security_update_required">Biometrische Sicherheitsaktualisierung erforderlich.</string>
<string name="configure_biometric">Es sind weder Biometrie- noch Geräteanmeldedaten registriert.</string> <string name="configure_biometric">Es sind weder Biometrie- noch Geräteanmeldedaten registriert.</string>
<string name="registration_mode">Registrierungsmodus</string>
<string name="save_mode">Speichermodus</string> <string name="save_mode">Speichermodus</string>
<string name="search_mode">Suchmodus</string> <string name="search_mode">Suchmodus</string>
<string name="error_registration_read_only">Speichern eines neuen Elements in einer schreibgeschützten Datenbank ist unzulässig.</string> <string name="error_registration_read_only">Speichern eines neuen Elements in einer schreibgeschützten Datenbank ist unzulässig.</string>

View File

@@ -19,7 +19,7 @@
--><resources> --><resources>
<string name="feedback">Σχόλια</string> <string name="feedback">Σχόλια</string>
<string name="homepage">Αρχική Σελίδα</string> <string name="homepage">Αρχική Σελίδα</string>
<string name="about_description">Το KeePassDX είναι μία εφαρμογή Android του διαχειριστή κωδικών KeePass</string> <string name="about_description">Υλοποίηση του διαχειριστή κωδικών πρόσβασης KeePass για Android.</string>
<string name="accept">Αποδοχή</string> <string name="accept">Αποδοχή</string>
<string name="add_entry">Προσθήκη καταχώρησης</string> <string name="add_entry">Προσθήκη καταχώρησης</string>
<string name="add_group">Προσθήκη ομάδας</string> <string name="add_group">Προσθήκη ομάδας</string>
@@ -479,7 +479,6 @@
<string name="biometric_security_update_required">Απαιτείται ενημέρωση βιομετρικής ασφάλειας.</string> <string name="biometric_security_update_required">Απαιτείται ενημέρωση βιομετρικής ασφάλειας.</string>
<string name="configure_biometric">Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο.</string> <string name="configure_biometric">Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο.</string>
<string name="warning_empty_recycle_bin">Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης;</string> <string name="warning_empty_recycle_bin">Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης;</string>
<string name="registration_mode">Τρόπος εγγραφής</string>
<string name="save_mode">Λειτουργία αποθήκευσης</string> <string name="save_mode">Λειτουργία αποθήκευσης</string>
<string name="search_mode">Λειτουργία αναζήτησης</string> <string name="search_mode">Λειτουργία αναζήτησης</string>
<string name="error_registration_read_only">Η αποθήκευση ενός νέου αντικειμένου δεν επιτρέπεται σε μια βάση δεδομένων μόνο για ανάγνωση.</string> <string name="error_registration_read_only">Η αποθήκευση ενός νέου αντικειμένου δεν επιτρέπεται σε μια βάση δεδομένων μόνο για ανάγνωση.</string>

View File

@@ -21,7 +21,7 @@
--><resources> --><resources>
<string name="feedback">Comentarios</string> <string name="feedback">Comentarios</string>
<string name="homepage">Página de inicio</string> <string name="homepage">Página de inicio</string>
<string name="about_description">Implementación para Android del gestor de contraseñas KeePass</string> <string name="about_description">Implementación para Android del gestor de contraseñas KeePass.</string>
<string name="accept">Aceptar</string> <string name="accept">Aceptar</string>
<string name="add_entry">Añadir apunte</string> <string name="add_entry">Añadir apunte</string>
<string name="add_group">Añadir grupo</string> <string name="add_group">Añadir grupo</string>
@@ -452,7 +452,6 @@
<string name="configure_biometric">No se ha inscrito ninguna credencial biométrica o del dispositivo.</string> <string name="configure_biometric">No se ha inscrito ninguna credencial biométrica o del dispositivo.</string>
<string name="warning_empty_keyfile_explanation">El contenido del archivo de clave nunca debe modificarse y, en el mejor de los casos, debe contener datos generados al azar.</string> <string name="warning_empty_keyfile_explanation">El contenido del archivo de clave nunca debe modificarse y, en el mejor de los casos, debe contener datos generados al azar.</string>
<string name="warning_empty_recycle_bin">¿Borrar permanentemente todos los nodos de la papelera de reciclaje\?</string> <string name="warning_empty_recycle_bin">¿Borrar permanentemente todos los nodos de la papelera de reciclaje\?</string>
<string name="registration_mode">Modo de registro</string>
<string name="save_mode">Modo de guardado</string> <string name="save_mode">Modo de guardado</string>
<string name="search_mode">Modo de búsqueda</string> <string name="search_mode">Modo de búsqueda</string>
<string name="contains_duplicate_uuid_procedure">¿Solucionar el problema generando nuevos UUID para que los duplicados continúen?</string> <string name="contains_duplicate_uuid_procedure">¿Solucionar el problema generando nuevos UUID para que los duplicados continúen?</string>
@@ -690,4 +689,5 @@
<string name="generate_keyfile">Generar archivo de claves</string> <string name="generate_keyfile">Generar archivo de claves</string>
<string name="recursive_number_entries_title">Número recursivo de entradas</string> <string name="recursive_number_entries_title">Número recursivo de entradas</string>
<string name="hide_templates_summary">Las plantillas no se muestran</string> <string name="hide_templates_summary">Las plantillas no se muestran</string>
<string name="error_otp_secret_length">La clave secreta debe tener al menos %1$d caracteres.</string>
</resources> </resources>

View File

@@ -383,7 +383,6 @@
<string name="search_mode">Otsinguviis</string> <string name="search_mode">Otsinguviis</string>
<string name="save_mode">Salvestusviis</string> <string name="save_mode">Salvestusviis</string>
<string name="selection_mode">Valikuviis</string> <string name="selection_mode">Valikuviis</string>
<string name="registration_mode">Registreerimisviis</string>
<string name="invalid_credentials">Salasõna või võtmefaili ei õnnestunud lugeda.</string> <string name="invalid_credentials">Salasõna või võtmefaili ei õnnestunud lugeda.</string>
<string name="protection">Kaitse</string> <string name="protection">Kaitse</string>
<string name="underline">Allajoonitud</string> <string name="underline">Allajoonitud</string>

View File

@@ -355,7 +355,6 @@
<string name="read_only_warning">Zure fitxategi nabigatzailearen arabera, KeePassDXek ez du baimenduta datuak idaztea.</string> <string name="read_only_warning">Zure fitxategi nabigatzailearen arabera, KeePassDXek ez du baimenduta datuak idaztea.</string>
<string name="save_mode">Gorde modua</string> <string name="save_mode">Gorde modua</string>
<string name="selection_mode">Hautaketa modua</string> <string name="selection_mode">Hautaketa modua</string>
<string name="registration_mode">Erregistro-modua</string>
<string name="remember_database_locations_title">Gogoratu datu-baseen kokalekuak</string> <string name="remember_database_locations_title">Gogoratu datu-baseen kokalekuak</string>
<string name="remember_database_locations_summary">Erregistratu datu-baseen kokapenak</string> <string name="remember_database_locations_summary">Erregistratu datu-baseen kokapenak</string>
<string name="remember_keyfile_locations_title">Gogoratu fitxategi-gakoen kokapenak</string> <string name="remember_keyfile_locations_title">Gogoratu fitxategi-gakoen kokapenak</string>

View File

@@ -271,7 +271,6 @@
<string name="import_app_properties_summary">یک فایل برای وارد کردن ویژگی های برنامه انتخاب کنید</string> <string name="import_app_properties_summary">یک فایل برای وارد کردن ویژگی های برنامه انتخاب کنید</string>
<string name="import_app_properties_title">وارد کردن ویژگی های برنامه</string> <string name="import_app_properties_title">وارد کردن ویژگی های برنامه</string>
<string name="export_app_properties_title">صادر کردن ویژگی های برنامه</string> <string name="export_app_properties_title">صادر کردن ویژگی های برنامه</string>
<string name="registration_mode">حالت ثبت</string>
<string name="save_mode">حالت ذخیره</string> <string name="save_mode">حالت ذخیره</string>
<string name="search_mode">حالت جستجو</string> <string name="search_mode">حالت جستجو</string>
<string name="menu_external_icon">نماد خارجی</string> <string name="menu_external_icon">نماد خارجی</string>

View File

@@ -431,7 +431,6 @@
<string name="warning_large_keyfile">Ei ole suositeltavaa lisätä suurta avantiedostos, sillä se voi estää tietokantaa avautumasta.</string> <string name="warning_large_keyfile">Ei ole suositeltavaa lisätä suurta avantiedostos, sillä se voi estää tietokantaa avautumasta.</string>
<string name="warning_empty_keyfile_explanation">Avaintiedoston sisältöä ei tulisi koskaan muuttaa, ja parhaassa tapauksessa sen tulisi sisältää satunnaisesti tuotettua tietoa.</string> <string name="warning_empty_keyfile_explanation">Avaintiedoston sisältöä ei tulisi koskaan muuttaa, ja parhaassa tapauksessa sen tulisi sisältää satunnaisesti tuotettua tietoa.</string>
<string name="save_mode">Tallennustila</string> <string name="save_mode">Tallennustila</string>
<string name="registration_mode">Rekisteröintitila</string>
<string name="export_app_properties_summary">Luo tiedosto, johon sovellusasetukset viedään</string> <string name="export_app_properties_summary">Luo tiedosto, johon sovellusasetukset viedään</string>
<string name="export_app_properties_title">Vie sovellusasetukset</string> <string name="export_app_properties_title">Vie sovellusasetukset</string>
<string name="description_app_properties">KeePassDX ominaisuudet sovellusasetusten hallintaan</string> <string name="description_app_properties">KeePassDX ominaisuudet sovellusasetusten hallintaan</string>

Some files were not shown because too many files have changed in this diff Show More