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.
labels: ["bug"]
body:

View File

@@ -1,5 +1,17 @@
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)
* 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"
minSdkVersion 19
targetSdkVersion 35
versionCode = 142
versionName = "4.2.0beta02"
versionCode = 145
versionName = "4.2.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -178,18 +178,22 @@
<activity
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
android:theme="@style/Theme.Transparent" />
android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
android:theme="@style/Theme.Transparent"
android:exported="false"
android:excludeFromRecents="true" />
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:excludeFromRecents="true"/>
android:exported="false"
android:excludeFromRecents="true" />
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent"
android:launchMode="singleInstance"
android:exported="true">
android:exported="true"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
@@ -208,8 +212,8 @@
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:excludeFromRecents="true"
android:exported="false"
android:excludeFromRecents="true"
tools:targetApi="upside_down_cake" />
<service
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.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.EnumSet
import java.util.UUID
class EntryActivity : DatabaseLockActivity() {
private var footer: ViewGroup? = null
private var container: View? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null
@@ -135,6 +137,7 @@ class EntryActivity : DatabaseLockActivity() {
// Get views
footer = findViewById(R.id.activity_entry_footer)
container = findViewById(R.id.activity_entry_container)
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar)
@@ -150,8 +153,12 @@ class EntryActivity : DatabaseLockActivity() {
setTransparentNavigationBar {
// To fix margin with API 27
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
container?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}
// Empty title
@@ -305,11 +312,11 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.historySelected.observe(this) { historySelected ->
mDatabase?.let { database ->
launch(
this,
database,
historySelected.nodeId,
historySelected.historyPosition,
mEntryActivityResultLauncher
activity = this,
database = database,
entryId = historySelected.nodeId,
historyPosition = historySelected.historyPosition,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
@@ -323,9 +330,8 @@ class EntryActivity : DatabaseLockActivity() {
return coordinatorLayout
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database)
}
@@ -471,11 +477,12 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_edit -> {
mDatabase?.let { database ->
mMainEntryId?.let { entryId ->
EntryEditActivity.launchToUpdate(
this,
database,
entryId,
mEntryActivityResultLauncher
EntryEditActivity.launch(
activity = this,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
@@ -513,7 +520,7 @@ class EntryActivity : DatabaseLockActivity() {
// Transit data in previous Activity after an update
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(Activity.RESULT_OK, this)
setResult(RESULT_OK, this)
}
super.finish()
}
@@ -527,34 +534,22 @@ class EntryActivity : DatabaseLockActivity() {
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
/**
* Open standard Entry activity
* Open standard or history Entry activity
*/
fun launch(activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
fun launch(
activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
historyPosition: Int? = null,
activityResultLauncher: ActivityResultLauncher<Intent>
) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
activityResultLauncher.launch(intent)
}
}
}
/**
* 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)
historyPosition?.let {
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
}
activityResultLauncher.launch(intent)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,6 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
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.deviceUnlockError
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -128,9 +125,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -310,26 +304,20 @@ class MainCredentialActivity : DatabaseModeActivity() {
mDatabaseFileUri?.let { databaseFileUri ->
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
if (database != null) {
// Trying to load another database
if (mDatabaseFileUri != null
&& database.fileUri != null
&& mDatabaseFileUri != database.fileUri) {
Toast.makeText(this,
R.string.warning_database_already_opened,
Toast.LENGTH_LONG
).show()
}
launchGroupActivityIfLoaded(database)
// Trying to load another database
if (mDatabaseFileUri != null
&& database.fileUri != null
&& mDatabaseFileUri != database.fileUri) {
Toast.makeText(this,
R.string.warning_database_already_opened,
Toast.LENGTH_LONG
).show()
}
launchGroupActivityIfLoaded(database)
}
override fun onDatabaseActionFinished(
@@ -514,10 +502,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
val password = intent.getStringExtra(KEY_PASSWORD)
// Consume the intent extra password
intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) {
mainCredentialView?.populatePasswordTextView(password)
}
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
if (launchImmediately) {
loadDatabase()
} else {
@@ -572,10 +561,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
clearCredentialsViews()
}
if (mReadOnly && (
mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
) {
if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
Log.e(TAG, getString(R.string.error_save_read_only))
Snackbar.make(coordinatorLayout,
R.string.error_save_read_only,
@@ -599,7 +585,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUUID: Boolean) {
loadDatabase(
mDatabaseViewModel.loadDatabase(
databaseUri,
mainCredential,
readOnly,
@@ -752,11 +738,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit) {
private fun buildAndLaunchIntent(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit
) {
val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null)
@@ -773,10 +761,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/
@Throws(FileNotFoundException::class)
fun launch(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?) {
fun launch(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent)
}
@@ -789,112 +779,56 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/
@Throws(FileNotFoundException::class)
fun launchForSearchResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
fun launchForSearchResult(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult(
activity,
intent,
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
context = activity,
intent = intent,
searchInfo = 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(
activity: Activity,
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.setPositiveButton(android.R.string.ok) { _, _ ->
actionDatabaseListener?.validateDatabaseChanged()
actionDatabaseListener?.onDatabaseChangeValidated()
}
return builder.create()
}
@@ -76,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
}
interface ActionDatabaseChangedListener {
fun validateDatabaseChanged()
fun onDatabaseChangeValidated()
}
companion object {
@@ -86,9 +86,10 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
readOnly: Boolean
fun getInstance(
oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
readOnly: Boolean
)
: DatabaseChangedDialogFragment {
val fragment = DatabaseChangedDialogFragment()

View File

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

View File

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

View File

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

View File

@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
}
mCustomIcon?.let { 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.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl
@@ -258,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
showEmptyPasswordConfirmationDialog()
} else if (!error
&& hardwareKey != null
&& !HardwareKeyActivity.isHardwareKeyAvailable(
requireActivity(), hardwareKey, false)
&& !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
) {
// show hardware driver dialog if required
error = true

View File

@@ -4,36 +4,59 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
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.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected var mDatabase: ContextualDatabase? = null
protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
if (mDatabase == null || mDatabase != database) {
this.mDatabase = database
onDatabaseRetrieved(database)
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 -> {}
}
}
}
}
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
onDatabaseActionFinished(result.database, result.actionTask, result.result)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
}
}
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
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
}
protected fun buildNewBinaryAttachment(): BinaryData? {
return mDatabase?.buildNewBinaryAttachment()
}
}

View File

@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
val attachmentToUploadUri = it.attachmentToUploadUri
val fileName = it.fileName
buildNewBinaryAttachment()?.let { binaryAttachment ->
mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment
if ((!mAllowMultipleAttachments
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
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?.onListSizeChangedListener = { previousSize, newSize ->
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
}
}
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply {
threshold = 1
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?) {

View File

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

View File

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

View File

@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
CoroutineScope(Dispatchers.IO).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,
if (database?.allowCustomIcons == true) 2 else 1)
if (database.allowCustomIcons) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {

View File

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

View File

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

View File

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

View File

@@ -1,104 +1,240 @@
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 androidx.activity.result.contract.ActivityResultContracts
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.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
import com.kunzisoft.keepass.database.ProgressMessage
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
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 kotlinx.coroutines.launch
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
protected var mDatabase: ContextualDatabase? = null
protected val mDatabase: ContextualDatabase?
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?) {
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 ->
val databaseWasReloaded = database?.wasReloaded == true
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
finish()
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
database?.wasReloaded = false
onDatabaseRetrieved(database)
is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
startDatabasePermissionService(
uiState.bundle,
uiState.actionTask
)
}
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 ->
onDatabaseActionFinished(database, actionTask, result)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
// Nullable function
onUnknownDatabaseRetrieved(database)
database?.let {
onDatabaseRetrieved(database)
}
}
}
}
}
protected open fun showDatabaseDialog(): Boolean {
return true
}
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
mDatabase = null
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
/**
* Nullable function to retrieve a database
*/
open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
// optional method implementation
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// optional method implementation
}
open fun manageDatabaseInfo(): Boolean = true
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
mDatabaseViewModel.onActionFinished(database, actionTask, result)
// optional method implementation
}
fun createDatabase(
databaseUri: Uri,
mainCredential: MainCredential
private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
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(
databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
mDatabaseTaskProvider?.startDatabaseLoad(
databaseUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
fixDuplicateUuid
)
private fun startDialog(progressMessage: ProgressMessage) {
lifecycleScope.launch {
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = supportFragmentManager
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
}
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(
supportFragmentManager,
PROGRESS_TASK_DIALOG_TAG
)
}
updateDialog(progressMessage)
}
}
protected fun closeDatabase() {
mDatabase?.clearAndClose(this.getBinaryDir())
private fun updateDialog(progressMessage: ProgressMessage) {
progressTaskDialogFragment?.apply {
updateTitle(progressMessage.titleId)
updateMessage(progressMessage.messageId)
updateWarning(progressMessage.warningId)
setCancellable(progressMessage.cancelable)
}
}
override fun onResume() {
super.onResume()
mDatabaseTaskProvider?.registerProgressTask()
private fun stopDialog() {
progressTaskDialogFragment?.dismissAllowingStateLoss()
progressTaskDialogFragment = null
}
override fun onPause() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
protected open fun showDatabaseDialog(): Boolean {
return true
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
package com.kunzisoft.keepass.credentialprovider
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.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() {
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher(lockDatabase = true)
private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageSelectionResult(it)
}
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageRegistrationResult(it)
}
override fun applyCustomStyle(): Boolean {
return false
@@ -64,177 +75,103 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
// Retrieve selection mode
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) {
SpecialMode.SELECTION -> {
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
// To pass extra inline request
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest = bundle.getParcelableCompat(
KEY_INLINE_SUGGESTION
)
}
// Build search param
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
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)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
autofillLauncherViewModel.initialize()
lifecycleScope.launch {
// Initialize the parameters
autofillLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
AutofillLauncherViewModel.UIState.Loading -> {}
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
showBlockRestartMessage()
autofillLauncherViewModel.cancelResult()
}
// Remove bundle
intent.removeExtra(KEY_SELECTION_BUNDLE)
}
SpecialMode.REGISTRATION -> {
// To register info
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(
KEY_REGISTER_INFO
)
val searchInfo = SearchInfo(registerInfo?.searchInfo)
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchRegistration(database, searchInfo, registerInfo)
is AutofillLauncherViewModel.UIState.ShowReadOnlyMessage -> {
showReadOnlySaveMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
showAutofillSuggestionMessage()
}
}
else -> {
// Not an autofill call
setResult(RESULT_CANCELED)
finish()
}
}
lifecycleScope.launch {
// Retrieve the UI
autofillLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.UIState.Loading -> {}
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@AutofillLauncherActivity,
database = uiState.database,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillSelectionActivityResultLauncher,
)
}
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?,
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo) {
if (autofillComponent == null) {
setResult(RESULT_CANCELED)
finish()
} else if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = this
)) {
// If database is open
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Items found
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
finish()
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
GroupActivity.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()
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
private fun showBlockRestartMessage() {
// If item not allowed, show a toast
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
Toast.makeText(
applicationContext,
R.string.autofill_block_restart,
Toast.LENGTH_LONG
).show()
}
private fun showAutofillSuggestionMessage() {
Toast.makeText(
applicationContext,
R.string.autofill_inline_suggestions_keyboard,
Toast.LENGTH_SHORT
).show()
}
private fun showReadOnlySaveMessage() {
@@ -245,27 +182,21 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
private val TAG = AutofillLauncherActivity::class.java.name
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? {
fun getPendingIntentForSelection(
context: Context,
searchInfo: SearchInfo? = null,
autofillComponent: AutofillComponent
): PendingIntent? {
try {
return PendingIntent.getActivity(
context, 0,
context,
randomRequestCode(),
// Doesn't work with direct extra Parcelable (don't know why?)
// Wrap into a bundle to bypass the problem
Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
}
})
addSpecialMode(SpecialMode.SELECTION)
addSearchInfo(searchInfo)
addAutofillComponent(autofillComponent)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
@@ -279,14 +210,17 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
}
fun getPendingIntentForRegistration(context: Context,
registerInfo: RegisterInfo): PendingIntent? {
fun getPendingIntentForRegistration(
context: Context,
registerInfo: RegisterInfo
): PendingIntent? {
try {
return PendingIntent.getActivity(
context, 0,
context,
randomRequestCode(),
Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo)
addSpecialMode(SpecialMode.REGISTRATION)
addRegisterInfo(registerInfo)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
@@ -299,14 +233,5 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
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.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.core.net.toUri
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.toastError
@@ -53,8 +51,8 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
return false
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
if (keySelectionBundle != null) {
@@ -88,7 +86,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
}
launchSelection(database, sharedWebDomain, otpString)
launchSelection(database, null, otpString)
}
else -> {
if (database != null) {
@@ -110,11 +108,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
this.webDomain = sharedWebDomain
this.otpString = otpString
}
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
}
launch(database, searchInfo)
}
private fun launch(database: ContextualDatabase?,
@@ -133,11 +127,12 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = null,
database = openedDatabase,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else {
toastError(RegisterInReadOnlyDatabaseException())
@@ -153,11 +148,12 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
)
},
{ autoSearch ->
GroupActivity.launchForKeyboardSelectionResult(
this,
openedDatabase,
searchInfo,
autoSearch
GroupActivity.launchForSelection(
context = this,
database = openedDatabase,
typeMode = TypeMode.MAGIKEYBOARD,
searchInfo = searchInfo,
autoSearch = autoSearch
)
}
)
@@ -174,21 +170,23 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = null,
database = openedDatabase,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else {
toastError(RegisterInReadOnlyDatabaseException())
}
} else if (searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(
this,
openedDatabase,
searchInfo,
false
GroupActivity.launchForSelection(
context = this,
database = openedDatabase,
typeMode = TypeMode.MAGIKEYBOARD,
searchInfo = searchInfo,
autoSearch = false
)
} else {
GroupActivity.launchForSearchResult(
@@ -202,14 +200,17 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
onDatabaseClosed = {
// If database not open
if (searchInfo.otpString != null) {
FileDatabaseSelectActivity.launchForSaveResult(
this,
searchInfo
FileDatabaseSelectActivity.launchForRegistration(
context = this,
activityResultLauncher = null,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(
this,
searchInfo
FileDatabaseSelectActivity.launchForSelection(
context = this,
typeMode = TypeMode.MAGIKEYBOARD,
searchInfo = searchInfo
)
} else {
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.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
@@ -44,14 +46,14 @@ import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
import java.util.UUID
@@ -79,10 +81,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
return false
}
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
@@ -105,61 +103,69 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
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 -> {
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?) {
super.onDatabaseRetrieved(database)
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database)
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
override fun onDatabaseActionFinished(
@@ -170,7 +176,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
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(getString(R.string.passkeys_missing_signature_app_ask_explanation))
.append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_question))
.toString()
)
setPositiveButton(android.R.string.ok) { _, _ ->
@@ -273,7 +282,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
): PendingIntent? {
return PendingIntent.getActivity(
context,
(Math.random() * Integer.MAX_VALUE).toInt(),
randomRequestCode(),
Intent(context, PasskeyLauncherActivity::class.java).apply {
addSpecialMode(specialMode)
addTypeMode(TypeMode.PASSKEY)

View File

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

View File

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

View File

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

View File

@@ -362,8 +362,8 @@ class StructureParser(private val structure: AssistStructure) {
if (result?.passwordId == null) {
usernameIdCandidate = autofillId
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,
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.adapters.FieldsAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
@@ -484,7 +485,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
// Populate Magikeyboard with entry
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode
EntrySelectionHelper.removeModesFromIntent(activity.intent)
activity.intent.removeModes()
activity.moveTaskToBack(true)
}
}

View File

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

View File

@@ -24,7 +24,6 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
@@ -44,6 +43,7 @@ import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.Signature
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
@@ -60,7 +60,6 @@ import com.kunzisoft.keepass.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
@@ -88,10 +87,7 @@ object PasskeyHelper {
private const val HMAC_TYPE = "HmacSHA256"
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
@@ -110,38 +106,6 @@ object PasskeyHelper {
private val internalSecureRandom: SecureRandom = SecureRandom()
/**
* Build the Passkey response for one entry
*/
fun Activity.buildPasskeyResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
try {
entryInfo.passkey?.let { passkey ->
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.addPasskey(passkey)
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
mReplyIntent.addNodeId(entryInfo.id)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
throw IOException("No passkey found")
}
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the passkey as result", e)
Toast.makeText(
this,
getString(R.string.error_passkey_result),
Toast.LENGTH_SHORT
).show()
setResult(Activity.RESULT_CANCELED)
}
}
/**
* Add an authentication code generated by an entry to the intent
*/
@@ -181,22 +145,6 @@ object PasskeyHelper {
return this.removeExtra(EXTRA_PASSKEY)
}
/**
* Add the search info to the intent
*/
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
searchInfo?.let {
putExtra(EXTRA_SEARCH_INFO, searchInfo)
}
}
/**
* Retrieve the search info from the intent
*/
fun Intent.retrieveSearchInfo(): SearchInfo? {
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
}
/**
* Add the app origin to the intent
*/
@@ -221,21 +169,37 @@ object PasskeyHelper {
}
/**
* Add the node id to the intent, useful for auto passkey selection
* Build the Passkey response for one entry
*/
fun Intent.addNodeId(nodeId: UUID?) {
nodeId?.let {
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
fun Activity.buildPasskeyResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
try {
entryInfo.passkey?.let { passkey ->
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.addPasskey(passkey)
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
mReplyIntent.addNodeId(entryInfo.id)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
throw IOException("No passkey found")
}
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the passkey as result", e)
Toast.makeText(
this,
getString(R.string.error_passkey_result),
Toast.LENGTH_SHORT
).show()
setResult(Activity.RESULT_CANCELED)
}
}
/**
* Retrieve the node id from the intent
*/
fun Intent.retrieveNodeId(): UUID? {
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
}
/**
* Check the timestamp and authentication code transmitted via PendingIntent
*/
@@ -424,11 +388,15 @@ object PasskeyHelper {
* Utility method to create a passkey and the associated creation request parameters
* [intent] allows to retrieve the request
* [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
*/
suspend fun retrievePasskeyCreationRequestParameters(
intent: Intent,
context: Context,
defaultBackupEligibility: Boolean?,
defaultBackupState: Boolean?,
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
) {
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
@@ -456,7 +424,9 @@ object PasskeyHelper {
privateKeyPem = privateKeyPem,
credentialId = b64Encode(credentialId),
userHandle = b64Encode(userHandle),
relyingParty = relyingParty
relyingParty = relyingParty,
backupEligibility = defaultBackupEligibility,
backupState = defaultBackupState
)
// create new entry in database
@@ -590,8 +560,8 @@ object PasskeyHelper {
requestOptions: PublicKeyCredentialRequestOptions,
clientDataResponse: ClientDataResponse,
passkey: Passkey,
backupEligibility: Boolean,
backupState: Boolean
defaultBackupEligibility: Boolean,
defaultBackupState: Boolean
): PublicKeyCredential {
val getCredentialResponse = FidoPublicKeyCredential(
id = passkey.credentialId,
@@ -599,8 +569,8 @@ object PasskeyHelper {
requestOptions = requestOptions,
userPresent = true,
userVerified = true,
backupEligibility = backupEligibility,
backupState = backupState,
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
backupState = passkey.backupState ?: defaultBackupState,
userHandle = passkey.userHandle,
privateKey = passkey.privateKeyPem,
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.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import androidx.lifecycle.AndroidViewModel
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.TypeMode
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.removePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
import com.kunzisoft.keepass.database.ContextualDatabase
@@ -56,22 +57,21 @@ import java.io.InvalidObjectException
import java.util.UUID
@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 mCreationParameters: PublicKeyCredentialCreationParameters? = null
private var mPasskey: Passkey? = null
private var mLockDatabaseAfterSelection: Boolean = false
private var mBackupEligibility: Boolean = true
private var mBackupState: Boolean = false
private var mLockDatabase: Boolean = true
private var isResultLauncherRegistered: Boolean = false
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = _uiState
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun initialize() {
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
}
@@ -79,19 +79,14 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
fun showAppPrivilegedDialog(
temptingApp: AndroidPrivilegedApp
) {
_uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
mUiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
}
fun showAppSignatureDialog(
temptingApp: AppOrigin,
nodeId: UUID
) {
_uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
}
fun showError(error: Throwable) {
Log.e(TAG, "Error on passkey launch", error)
_uiState.value = UIState.ShowError(error)
mUiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
}
fun saveCustomPrivilegedApp(
@@ -107,7 +102,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
context = getApplication(),
privilegedApps = listOf(temptingApp)
)
launchPasskeyAction(
launchAction(
intent = intent,
specialMode = specialMode,
database = database
@@ -139,54 +134,33 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
)
entryInfo.saveAppOrigin(database, temptingApp)
newEntry.setEntryInfo(database, entryInfo)
_uiState.value = UIState.UpdateEntry(
mUiState.value = UIState.UpdateEntry(
oldEntry = entry,
newEntry = newEntry
)
}
}
fun setResult(intent: Intent) {
// Remove the launcher register
isResultLauncherRegistered = false
_uiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_OK,
data = intent
)
override fun onExceptionOccurred(e: Throwable) {
if (e is PrivilegedAllowLists.PrivilegedException) {
showAppPrivilegedDialog(e.temptingApp)
} else {
super.onExceptionOccurred(e)
}
}
fun cancelResult() {
isResultLauncherRegistered = false
_uiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_CANCELED
)
}
fun launchPasskeyActionIfNeeded(
override fun launchActionIfNeeded(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
if (isResultLauncherRegistered.not()) {
isResultLauncherRegistered = true
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
if (e is PrivilegedAllowLists.PrivilegedException) {
showAppPrivilegedDialog(e.temptingApp)
} else {
showError(e)
}
}) {
launchPasskeyAction(intent, specialMode, database)
}
// Launch with database when a nodeId is present
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
super.launchActionIfNeeded(intent, specialMode, database)
}
}
/**
* Launch the main action to manage Passkey
*/
private suspend fun launchPasskeyAction(
override suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
@@ -194,6 +168,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
val nodeId = intent.retrieveNodeId()
intent.removeInfo()
intent.removeAppOrigin()
intent.removeNodeId()
checkSecurity(intent, nodeId)
when (specialMode) {
SpecialMode.SELECTION -> {
@@ -260,15 +237,19 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "No Passkey found for selection," +
"launch manual selection in opened database"
)
_uiState.value = UIState.LaunchGroupActivityForSelection(
database = openedDatabase
)
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
)
},
onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database")
_uiState.value =
UIState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
)
}
)
@@ -326,12 +307,12 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
appOrigin = appOrigin
),
passkey = passkey,
backupEligibility = mBackupEligibility,
backupState = mBackupState
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState
)
)
)
setResult(result)
setResult(result, lockDatabase = mLockDatabaseAfterSelection)
} catch (e: SignatureNotFoundException) {
// Request the dialog if signature exception
showAppSignatureDialog(e.temptingApp, nodeId)
@@ -340,9 +321,11 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
}
}
fun manageSelectionResult(
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 passkey", e)
@@ -380,8 +363,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
appOrigin = appOrigin
),
passkey = passkey,
backupEligibility = mBackupEligibility,
backupState = mBackupState
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState
)
)
)
@@ -389,7 +372,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
throw IOException("Usage parameters is null")
}
withContext(Dispatchers.Main) {
setResult(responseIntent)
setResult(responseIntent, lockDatabase = mLockDatabaseAfterSelection)
}
}
}
@@ -417,6 +400,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
retrievePasskeyCreationRequestParameters(
intent = intent,
context = getApplication(),
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState,
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
// Save the requested parameters
mPasskey = passkey
@@ -440,24 +425,26 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "Passkey found for registration, " +
"but launch manual registration for a new entry"
)
_uiState.value = UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
},
onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database")
_uiState.value = UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
},
onDatabaseClosed = {
Log.d(TAG, "Manual passkey registration in closed database")
_uiState.value =
UIState.LaunchFileDatabaseSelectActivityForRegistration(
mCredentialUiState.value =
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo,
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
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create registration response for passkey", e)
@@ -518,8 +505,10 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters = it,
backupEligibility = mBackupEligibility,
backupState = mBackupState
backupEligibility = passkey?.backupEligibility
?: mBackupEligibility,
backupState = passkey?.backupState
?: mBackupState
)
)
}
@@ -549,29 +538,6 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val temptingApp: AppOrigin,
val nodeId: UUID
): 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(
val oldEntry: Entry,
val newEntry: Entry

View File

@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.database
import android.Manifest
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
@@ -29,23 +28,15 @@ import android.content.Context.BIND_IMPORTANT
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat
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.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.kdf.KdfEngine
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.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
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_TEMPLATES_GROUP_TASK
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_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.putParcelableList
import kotlinx.coroutines.launch
import java.util.UUID
/**
@@ -103,121 +89,29 @@ import java.util.UUID
* Useful to retrieve a database instance and sending tasks commands
*/
class DatabaseTaskProvider(
private var context: Context,
private var showDialog: Boolean = true
private var context: Context
) {
// 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 onActionFinish: ((
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) -> Unit)? = null
private var intentDatabaseTask: Intent = Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
)
var onStartActionRequested: ((bundle: Bundle?, actionTask: String) -> Unit)? = null
var actionTaskListener: DatabaseTaskNotificationService.ActionTaskListener? = null
var databaseInfoListener: DatabaseTaskNotificationService.DatabaseInfoListener? = null
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
private var serviceConnection: ServiceConnection? = null
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
fun destroy() {
this.activity = null
this.onDatabaseRetrieved = null
this.onActionFinish = null
this.databaseTaskBroadcastReceiver = null
this.mBinder = null
this.serviceConnection = null
this.progressTaskDialogFragment = null
this.databaseChangedDialogFragment = null
}
private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
override fun onActionStarted(
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
)
}
}
}
}
fun onDatabaseChangeValidated() {
mBinder?.getService()?.saveDatabaseInfo()
}
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() {
actionTaskListener?.onActionStopped()
if (serviceConnection == null) {
serviceConnection = object : ServiceConnection {
override fun onBindingDied(name: ComponentName?) {
stopDialog()
actionTaskListener?.onActionStopped()
onDatabaseRetrieved?.invoke(null)
}
override fun onNullBinding(name: ComponentName?) {
stopDialog()
actionTaskListener?.onActionStopped()
onDatabaseRetrieved?.invoke(null)
}
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
@@ -290,21 +154,33 @@ class DatabaseTaskProvider(
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.addDatabaseListener(databaseListener)
service?.addDatabaseFileInfoListener(databaseInfoListener)
service?.addActionTaskListener(actionTaskListener)
databaseInfoListener?.let { infoListener ->
service?.addDatabaseFileInfoListener(infoListener)
}
actionTaskListener?.let { taskListener ->
service?.addActionTaskListener(taskListener)
}
}
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.removeActionTaskListener(actionTaskListener)
service?.removeDatabaseFileInfoListener(databaseInfoListener)
actionTaskListener?.let { taskListener ->
service?.removeActionTaskListener(taskListener)
}
databaseInfoListener?.let { infoListener ->
service?.removeDatabaseFileInfoListener(infoListener)
}
service?.removeDatabaseListener(databaseListener)
onDatabaseRetrieved?.invoke(null)
}
private fun bindService() {
initServiceConnection()
serviceConnection?.let {
context.bindService(
intentDatabaseTask,
Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
),
it,
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) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val contextActivity = activity
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()
onStartActionRequested?.invoke(bundle, actionTask) ?: run {
context.startDatabaseService(bundle, actionTask)
}
}
@@ -842,5 +669,21 @@ class DatabaseTaskProvider(
companion object {
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.XMLMalformedDatabaseException
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_RELYING_PARTY
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_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_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
}

View File

@@ -21,9 +21,16 @@ package com.kunzisoft.keepass.database.helper
import android.content.Context
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains
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 {
@@ -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]
*/
@@ -52,28 +129,31 @@ object SearchHelper {
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
onDatabaseClosed: () -> Unit
) {
// Do not place coroutine at start, bug in Passkey implementation
if (database == null || !database.loaded) {
onDatabaseClosed.invoke()
} else if (TimeoutHelper.checkTime(context)) {
var searchWithoutUI = false
if (searchInfo != null
&& !searchInfo.manualSelection
&& !searchInfo.containsOnlyNullValues()) {
// If search provide results
database.createVirtualGroupFromSearchInfo(
searchInfo,
MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {
searchWithoutUI = true
onItemsFound.invoke(database,
searchGroup.getChildEntriesInfo(database))
}
&& !searchInfo.containsOnlyNullValues()
) {
searchInfo.getSearchParametersFromSearchInfo(context) { searchParameters ->
// If search provide results
database.createVirtualGroupFromSearchInfo(
searchParameters = searchParameters,
max = MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {
onItemsFound.invoke(
database,
searchGroup.getChildEntriesInfo(database)
)
} else
onItemNotFound.invoke(database)
} ?: onItemNotFound.invoke(database)
}
}
if (!searchWithoutUI) {
} else
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) {
@Throws(IllegalArgumentException::class)
fun generatePassword(length: Int,
upperCase: Boolean,
lowerCase: Boolean,
digits: Boolean,
minus: Boolean,
underline: Boolean,
space: Boolean,
specials: Boolean,
brackets: Boolean,
extended: Boolean,
considerChars: String,
ignoreChars: String,
atLeastOneFromEach: Boolean,
excludeAmbiguousChar: Boolean): String {
fun generatePassword(
length: Int,
upperCase: Boolean,
lowerCase: Boolean,
digits: Boolean,
minus: Boolean,
underline: Boolean,
space: Boolean,
specials: Boolean,
brackets: Boolean,
extended: Boolean,
considerChars: String,
ignoreChars: String,
atLeastOneFromEach: Boolean,
excludeAmbiguousChar: Boolean
): String {
// Desired password length is 0 or less
if (length <= 0) {
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 UNDERLINE_CHAR = "_"
private const val SPACE_CHAR = " "
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
private const val SPECIAL_CHARS = "&/,^@.#:%\\='$!?*`;+\"|~"
private const val BRACKET_CHARS = "[]{}()<>"
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.StreamDirection
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.getParcelableExtraCompat
import kotlinx.coroutines.CoroutineScope
@@ -194,7 +195,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this,
0,
randomRequestCode(),
Intent().apply {
action = Intent.ACTION_VIEW
setDataAndType(attachmentNotification.uri,
@@ -208,7 +209,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
)
val pendingDeleteIntent = PendingIntent.getService(this,
0,
randomRequestCode(),
Intent(this, AttachmentFileNotificationService::class.java).apply {
// No action to delete the service
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.Type
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.SnapFileDatabaseInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
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_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION
@@ -175,7 +176,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
progressMessage: ProgressMessage
)
fun onActionStopped(
database: ContextualDatabase
database: ContextualDatabase? = null
)
fun onActionFinished(
database: ContextualDatabase,
@@ -550,7 +551,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Build Intents for notification action
val pendingDatabaseIntent = PendingIntent.getActivity(
this,
0,
randomRequestCode(),
Intent(this, GroupActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
@@ -675,6 +676,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
override fun actionOnLock() {
if (!TimeoutHelper.temporarilyDisableLock) {
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)
TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast
@@ -709,9 +716,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
notifyProgressMessage()
HardwareKeyActivity
.launchHardwareKeyActivity(
this@DatabaseTaskNotificationService,
hardwareKey,
seed
context = this@DatabaseTaskNotificationService,
hardwareKey = hardwareKey,
seed = seed
)
// Wait the response
mProgressMessage.apply {

View File

@@ -41,6 +41,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
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) {

View File

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

View File

@@ -19,13 +19,21 @@
*/
package com.kunzisoft.keepass.settings
import android.graphics.Color
import android.os.Bundle
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.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
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.element.Group
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.settings.preference.*
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
import com.kunzisoft.keepass.settings.preference.DialogColorPreference
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.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getSerializableCompat
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
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 mMergeDataAllowed: Boolean = false
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?) {
super.onViewCreated(view, savedInstanceState)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
mDatabase = database
view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded)
onDatabaseRetrieved(database)
}
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
onDatabaseActionFinished(it.database, it.actionTask, it.result)
viewLifecycleOwner.lifecycleScope.launch {
mDatabaseViewModel.databaseState.collect { database ->
view.resetAppTimeoutWhenViewTouchedOrFocused(
context = requireContext(),
databaseLoaded = database?.loaded
)
}
}
}
@@ -167,29 +223,26 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabaseViewModel.reloadDatabase(false)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseReadOnly = database?.isReadOnly == true
mMergeDataAllowed = database?.isMergeDataAllowed() == true
override fun onDatabaseRetrieved(database: ContextualDatabase) {
mDatabaseReadOnly = database.isReadOnly
mMergeDataAllowed = database.isMergeDataAllowed()
mDatabase?.let {
if (it.loaded) {
when (mScreen) {
Screen.DATABASE -> {
onCreateDatabasePreference(it)
}
Screen.DATABASE_SECURITY -> {
onCreateDatabaseSecurityPreference(it)
}
Screen.DATABASE_MASTER_KEY -> {
onCreateDatabaseMasterKeyPreference(it)
}
else -> {
}
if (database.loaded) {
when (mScreen) {
Screen.DATABASE -> {
onCreateDatabasePreference(database)
}
Screen.DATABASE_SECURITY -> {
onCreateDatabaseSecurityPreference(database)
}
Screen.DATABASE_MASTER_KEY -> {
onCreateDatabaseMasterKeyPreference(database)
}
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) {
newDefaultUsername
} else {
mDatabase?.defaultUsername = oldDefaultUsername
database.defaultUsername = oldDefaultUsername
oldDefaultUsername
}
dbDefaultUsernamePref?.summary = defaultUsernameToShow
@@ -471,7 +524,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newColor
} else {
mDatabase?.customColor = Color.parseColor(oldColor)
database.customColor = oldColor.toColorInt()
oldColor
}
dbCustomColorPref?.summary = defaultColorToShow
@@ -483,7 +536,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newCompression
} else {
mDatabase?.compressionAlgorithm = oldCompression
database.compressionAlgorithm = oldCompression
oldCompression
}
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
@@ -497,7 +550,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else {
oldRecycleBin
}
mDatabase?.setRecycleBin(recycleBinToShow)
database.setRecycleBin(recycleBinToShow)
refreshRecycleBinGroup(database)
}
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
@@ -509,7 +562,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else {
oldTemplatesGroup
}
mDatabase?.setTemplatesGroup(templatesGroupToShow)
database.setTemplatesGroup(templatesGroupToShow)
refreshTemplatesGroup(database)
}
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
@@ -519,7 +572,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newMaxHistoryItems
} else {
mDatabase?.historyMaxItems = oldMaxHistoryItems
database.historyMaxItems = oldMaxHistoryItems
oldMaxHistoryItems
}
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
@@ -531,7 +584,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newMaxHistorySize
} else {
mDatabase?.historyMaxSize = oldMaxHistorySize
database.historyMaxSize = oldMaxHistorySize
oldMaxHistorySize
}
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
@@ -549,7 +602,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newEncryption
} else {
mDatabase?.encryptionAlgorithm = oldEncryption
database.encryptionAlgorithm = oldEncryption
oldEncryption
}
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
@@ -561,7 +614,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newKeyDerivationEngine
} else {
mDatabase?.kdfEngine = oldKeyDerivationEngine
database.kdfEngine = oldKeyDerivationEngine
oldKeyDerivationEngine
}
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
@@ -578,7 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newIterations
} else {
mDatabase?.numberKeyEncryptionRounds = oldIterations
database.numberKeyEncryptionRounds = oldIterations
oldIterations
}
mRoundPref?.summary = roundsToShow.toString()
@@ -590,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newMemoryUsage
} else {
mDatabase?.memoryUsage = oldMemoryUsage
database.memoryUsage = oldMemoryUsage
oldMemoryUsage
}
mMemoryPref?.summary = memoryToShow.toString()
@@ -602,7 +655,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newParallelism
} else {
mDatabase?.parallelism = oldParallelism
database.parallelism = oldParallelism
oldParallelism
}
mParallelismPref?.summary = parallelismToShow.toString()

View File

@@ -108,7 +108,7 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.auto_focus_search_default))
}
fun searchSubdomains(context: Context): Boolean {
fun searchSubDomains(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
context.resources.getBoolean(R.bool.subdomain_search_default))
@@ -352,6 +352,8 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.search_option_username_default))
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
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),
context.resources.getBoolean(R.bool.search_option_url_default))
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
@@ -389,6 +391,8 @@ object PreferencesUtil {
searchParameters.searchInUsernames)
putBoolean(context.getString(R.string.search_option_password_key),
searchParameters.searchInPasswords)
putBoolean(context.getString(R.string.search_option_application_id_key),
searchParameters.searchInAppIds)
putBoolean(context.getString(R.string.search_option_url_key),
searchParameters.searchInUrls)
putBoolean(context.getString(R.string.search_option_expired_key),
@@ -686,6 +690,12 @@ object PreferencesUtil {
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 {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
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_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.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_inline_suggestions_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
/* TODO Settings nav bar
setTransparentNavigationBar {
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
coordinatorLayout?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}*/
mExternalFileHelper = ExternalFileHelper(this)
@@ -155,10 +159,6 @@ open class SettingsActivity
return coordinatorLayout
}
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
return false
}
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
@@ -188,7 +188,7 @@ open class SettingsActivity
}
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
assignPassword(mainCredential)
assignMainCredential(mainCredential)
}
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,31 +34,29 @@ class DatabaseMaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePrefere
setExplanationText(R.string.max_history_size_summary)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
database?.historyMaxSize?.let { maxItemsDatabase ->
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
if (dataByte.number >= 0) {
setUnitText(dataByte.format.stringId)
} else {
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)
override fun onDatabaseRetrieved(database: ContextualDatabase) {
val maxItemsDatabase = database.historyMaxSize
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
if (dataByte.number >= 0) {
setUnitText(dataByte.format.stringId)
} else {
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)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,9 +32,8 @@ class DatabaseRoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
explanationText = getString(R.string.rounds_explanation)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString()
override fun onDatabaseRetrieved(database: ContextualDatabase) {
inputText = database.numberKeyEncryptionRounds.toString()
}
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.os.Bundle
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.keepass.activities.legacy.DatabaseRetrieval
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.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseSavePreferenceDialogFragmentCompat
: InputPreferenceDialogFragmentCompat(), DatabaseRetrieval {
private var mDatabaseAutoSaveEnable = true
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: ContextualDatabase? = null
protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -47,18 +52,32 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseViewModel.database.observe(this) { database ->
onDatabaseRetrieved(database)
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 onResume() {
super.onResume()
onDatabaseRetrieved(mDatabase)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
this.mDatabase = database
}
override fun onDatabaseActionFinished(
@@ -77,8 +96,10 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
// To inherit to save element in database
}
protected fun saveColor(oldColor: Int?,
newColor: Int?) {
protected fun saveColor(
oldColor: Int?,
newColor: Int?
) {
val oldColorString = if (oldColor != null)
ChromaUtil.getFormattedColorString(oldColor, false)
else
@@ -87,77 +108,158 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
ChromaUtil.getFormattedColorString(newColor, false)
else
""
mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable)
mDatabaseViewModel.saveColor(
oldColorString,
newColorString,
mDatabaseAutoSaveEnable
)
}
protected fun saveCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm
protected fun saveCompression(
oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm
) {
mDatabaseViewModel.saveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
mDatabaseViewModel.saveCompression(
oldCompression,
newCompression,
mDatabaseAutoSaveEnable
)
}
protected fun saveDefaultUsername(oldUsername: String,
newUsername: String) {
mDatabaseViewModel.saveDefaultUsername(oldUsername, newUsername, mDatabaseAutoSaveEnable)
protected fun saveDefaultUsername(
oldUsername: String,
newUsername: String
) {
mDatabaseViewModel.saveDefaultUsername(
oldUsername,
newUsername,
mDatabaseAutoSaveEnable
)
}
protected fun saveDescription(oldDescription: String,
newDescription: String) {
mDatabaseViewModel.saveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
protected fun saveDescription(
oldDescription: String,
newDescription: String
) {
mDatabaseViewModel.saveDescription(
oldDescription,
newDescription,
mDatabaseAutoSaveEnable
)
}
protected fun saveEncryption(oldEncryption: EncryptionAlgorithm,
newEncryptionAlgorithm: EncryptionAlgorithm) {
mDatabaseViewModel.saveEncryption(oldEncryption, newEncryptionAlgorithm, mDatabaseAutoSaveEnable)
protected fun saveEncryption(
oldEncryption: EncryptionAlgorithm,
newEncryptionAlgorithm: EncryptionAlgorithm
) {
mDatabaseViewModel.saveEncryption(
oldEncryption,
newEncryptionAlgorithm,
mDatabaseAutoSaveEnable
)
}
protected fun saveKeyDerivation(oldKeyDerivation: KdfEngine,
newKeyDerivation: KdfEngine) {
mDatabaseViewModel.saveKeyDerivation(oldKeyDerivation, newKeyDerivation, mDatabaseAutoSaveEnable)
protected fun saveKeyDerivation(
oldKeyDerivation: KdfEngine,
newKeyDerivation: KdfEngine
) {
mDatabaseViewModel.saveKeyDerivation(
oldKeyDerivation,
newKeyDerivation,
mDatabaseAutoSaveEnable
)
}
protected fun saveName(oldName: String,
newName: String) {
mDatabaseViewModel.saveName(oldName, newName, mDatabaseAutoSaveEnable)
protected fun saveName(
oldName: String,
newName: String
) {
mDatabaseViewModel.saveName(
oldName,
newName,
mDatabaseAutoSaveEnable
)
}
protected fun saveRecycleBin(oldGroup: Group?,
newGroup: Group?) {
mDatabaseViewModel.saveRecycleBin(oldGroup, newGroup, mDatabaseAutoSaveEnable)
protected fun saveRecycleBin(
oldGroup: Group?,
newGroup: Group?
) {
mDatabaseViewModel.saveRecycleBin(
oldGroup,
newGroup,
mDatabaseAutoSaveEnable
)
}
protected fun removeUnlinkedData() {
mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable)
}
protected fun saveTemplatesGroup(oldGroup: Group?,
newGroup: Group?) {
mDatabaseViewModel.saveTemplatesGroup(oldGroup, newGroup, mDatabaseAutoSaveEnable)
protected fun saveTemplatesGroup(
oldGroup: Group?,
newGroup: Group?
) {
mDatabaseViewModel.saveTemplatesGroup(
oldGroup,
newGroup,
mDatabaseAutoSaveEnable
)
}
protected fun saveMaxHistoryItems(oldNumber: Int,
newNumber: Int) {
mDatabaseViewModel.saveMaxHistoryItems(oldNumber, newNumber, mDatabaseAutoSaveEnable)
protected fun saveMaxHistoryItems(
oldNumber: Int,
newNumber: Int
) {
mDatabaseViewModel.saveMaxHistoryItems(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
}
protected fun saveMaxHistorySize(oldNumber: Long,
newNumber: Long) {
mDatabaseViewModel.saveMaxHistorySize(oldNumber, newNumber, mDatabaseAutoSaveEnable)
protected fun saveMaxHistorySize(
oldNumber: Long,
newNumber: Long
) {
mDatabaseViewModel.saveMaxHistorySize(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
}
protected fun saveMemoryUsage(oldNumber: Long,
newNumber: Long) {
mDatabaseViewModel.saveMemoryUsage(oldNumber, newNumber, mDatabaseAutoSaveEnable)
protected fun saveMemoryUsage(
oldNumber: Long,
newNumber: Long
) {
mDatabaseViewModel.saveMemoryUsage(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
}
protected fun saveParallelism(oldNumber: Long,
newNumber: Long) {
mDatabaseViewModel.saveParallelism(oldNumber, newNumber, mDatabaseAutoSaveEnable)
protected fun saveParallelism(
oldNumber: Long,
newNumber: Long
) {
mDatabaseViewModel.saveParallelism(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
}
protected fun saveIterations(oldNumber: Long,
newNumber: Long) {
mDatabaseViewModel.saveIterations(oldNumber, newNumber, mDatabaseAutoSaveEnable)
protected fun saveIterations(
oldNumber: Long,
newNumber: Long
) {
mDatabaseViewModel.saveIterations(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
}
companion object {

View File

@@ -48,12 +48,9 @@ class DatabaseTemplatesGroupPreferenceDialogFragmentCompat
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
database?.let {
mGroupTemplates = database.templatesGroup
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
mGroupTemplates = database.templatesGroup
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
}
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.credentialprovider.passkey.data.AndroidPrivilegedApp
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 {
fun randomRequestCode(): Int {
return (Math.random() * Integer.MAX_VALUE).toInt()
}
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
try {
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)
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
val packageManager = context.packageManager

View File

@@ -19,30 +19,39 @@
*/
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 IOActionTask<T>(
private val action: () -> T ,
private val afterActionListener: ((T?) -> Unit)? = null) {
private val mainScope = CoroutineScope(Dispatchers.Main)
private val action: () -> T,
private val onActionComplete: ((T?) -> Unit)? = null,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
private val exceptionHandler: CoroutineExceptionHandler? = null
) {
fun execute() {
mainScope.launch {
scope.launch(exceptionHandler ?: EmptyCoroutineContext) {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<T?> = async {
try {
action.invoke()
} catch (e: Exception) {
e.printStackTrace()
null
}
exceptionHandler?.let {
action.invoke()
} ?: try {
action.invoke()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
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.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.ImageView
@@ -30,8 +29,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
private var searchTitle: CompoundButton
private var searchUsername: CompoundButton
private var searchPassword: CompoundButton
private var searchApplicationId: CompoundButton
private var searchURL: CompoundButton
private var searchByURLDomain: Boolean = false
private var searchByURLSubDomain: Boolean = false
private var searchExpired: CompoundButton
private var searchNotes: CompoundButton
private var searchOther: CompoundButton
@@ -50,8 +51,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
this.searchInTitles = searchTitle.isChecked
this.searchInUsernames = searchUsername.isChecked
this.searchInPasswords = searchPassword.isChecked
this.searchInAppIds = searchApplicationId.isChecked
this.searchInUrls = searchURL.isChecked
this.searchByDomain = searchByURLDomain
this.searchBySubDomain = searchByURLSubDomain
this.searchInExpired = searchExpired.isChecked
this.searchInNotes = searchNotes.isChecked
this.searchInOther = searchOther.isChecked
@@ -71,8 +74,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchTitle.isChecked = value.searchInTitles
searchUsername.isChecked = value.searchInUsernames
searchPassword.isChecked = value.searchInPasswords
searchApplicationId.isChecked = value.searchInAppIds
searchURL.isChecked = value.searchInUrls
searchByURLDomain = value.searchByDomain
searchByURLSubDomain = value.searchBySubDomain
searchExpired.isChecked = value.searchInExpired
searchNotes.isChecked = value.searchInNotes
searchOther.isChecked = value.searchInOther
@@ -87,7 +92,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
// To recalculate height
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
searchAdvanceFiltersContainer?.expand(
false,
searchAdvanceFiltersContainer?.getFullHeight()
@@ -110,6 +115,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchTitle = findViewById(R.id.search_chip_title)
searchUsername = findViewById(R.id.search_chip_username)
searchPassword = findViewById(R.id.search_chip_password)
searchApplicationId = findViewById(R.id.search_chip_application_id)
searchURL = findViewById(R.id.search_chip_url)
searchExpired = findViewById(R.id.search_chip_expires)
searchNotes = findViewById(R.id.search_chip_note)
@@ -125,7 +131,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
// Expand menu with button
searchExpandButton.setOnClickListener {
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE
val isVisible = searchAdvanceFiltersContainer?.visibility == VISIBLE
if (isVisible)
closeAdvancedFilters()
else
@@ -156,6 +162,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchParameters.searchInPasswords = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchApplicationId.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInAppIds = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchURL.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInUrls = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
@@ -200,10 +210,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
}
fun setCurrentGroupText(text: String) {
fun setCurrentGroupText(text: String?) {
val maxChars = 12
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) + ""
else -> text
}
@@ -213,6 +223,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchOther.isVisible = available
}
fun availableApplicationIds(available: Boolean) {
searchApplicationId.isVisible = available
}
fun availableTags(available: Boolean) {
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) {
when (visibility) {
View.VISIBLE -> {
searchAdvanceFiltersContainer?.visibility = View.GONE
VISIBLE -> {
searchAdvanceFiltersContainer?.visibility = GONE
searchContainer.showByFading()
}
else -> {
searchContainer.hideByFading()
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
searchAdvanceFiltersContainer?.visibility = View.INVISIBLE
if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
searchAdvanceFiltersContainer?.visibility = INVISIBLE
searchAdvanceFiltersContainer?.collapse()
}
}

View File

@@ -24,7 +24,6 @@ import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Paint
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.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import java.util.EnumSet
/**
@@ -317,9 +317,7 @@ fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
@Suppress("DEPRECATION")
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
// Only in portrait
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
if (applyToStatusBar) {
@@ -335,7 +333,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
/**
* 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 ->
var consumed = false
@@ -351,52 +349,78 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
}
}
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
when (position) {
WindowInsetPosition.TOP -> {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
or WindowInsetsCompat.Type.ime())
val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
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
}
}
}
WindowInsetPosition.LEGIT_TOP -> {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = 0
}
}
}
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
if (wantEndMargins) {
if (isRtl) {
leftMargin = insets.left
} else {
rightMargin = insets.right
}
}
}
}
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 (consumed) WindowInsetsCompat.CONSUMED else windowInsets
}
}
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
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import android.app.Application
import android.net.Uri
import android.os.Bundle
import androidx.lifecycle.AndroidViewModel
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.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Entry
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.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 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 _database = MutableLiveData<ContextualDatabase?>()
private val mDatabaseState = MutableStateFlow<ContextualDatabase?>(null)
val databaseState: StateFlow<ContextualDatabase?> = mDatabaseState
val actionFinished : LiveData<ActionResult> get() = _actionFinished
private val _actionFinished = SingleLiveEvent<ActionResult>()
val database: ContextualDatabase?
get() = databaseState.value
val saveDatabase : LiveData<Boolean> get() = _saveDatabase
private val _saveDatabase = SingleLiveEvent<Boolean>()
private val mActionState = MutableStateFlow<ActionState>(ActionState.Loading)
val actionState: StateFlow<ActionState> = mActionState
val mergeDatabase : LiveData<Boolean> get() = _mergeDatabase
private val _mergeDatabase = SingleLiveEvent<Boolean>()
private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider(
context = application
)
val reloadDatabase : LiveData<Boolean> get() = _reloadDatabase
private val _reloadDatabase = SingleLiveEvent<Boolean>()
init {
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
private val _saveName = SingleLiveEvent<SuperString>()
override fun onActionUpdated(
database: ContextualDatabase,
progressMessage: ProgressMessage
) {
mActionState.value = ActionState.OnDatabaseActionUpdated(database, progressMessage)
}
val saveDescription : LiveData<SuperString> get() = _saveDescription
private val _saveDescription = SingleLiveEvent<SuperString>()
override fun onActionStopped(database: ContextualDatabase?) {
mActionState.value = ActionState.OnDatabaseActionStopped(database)
}
val saveDefaultUsername : LiveData<SuperString> get() = _saveDefaultUsername
private val _saveDefaultUsername = SingleLiveEvent<SuperString>()
override fun onActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
mActionState.value = ActionState.OnDatabaseActionFinished(database, actionTask, result)
}
}
val saveColor : LiveData<SuperString> get() = _saveColor
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
mDatabaseTaskProvider.registerProgressTask()
}
fun onActionFinished(database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result) {
this._actionFinished.value = ActionResult(database, actionTask, result)
/*
* Main database actions
*/
fun loadDatabase(
databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
mDatabaseTaskProvider.startDatabaseLoad(
databaseUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
fixDuplicateUuid
)
}
fun saveDatabase(save: Boolean) {
_saveDatabase.value = save
fun createDatabase(
databaseUri: Uri,
mainCredential: MainCredential
) {
mDatabaseTaskProvider.startDatabaseCreate(databaseUri, mainCredential)
}
fun mergeDatabase(save: Boolean) {
_mergeDatabase.value = save
fun assignMainCredential(
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) {
_reloadDatabase.value = fixDuplicateUuid
mDatabaseTaskProvider.askToStartDatabaseReload(
conditionToAsk = database?.dataModifiedSinceLastLoading != false
) {
mDatabaseTaskProvider.startDatabaseReload(fixDuplicateUuid)
}
}
fun saveName(oldValue: String,
newValue: String,
save: Boolean) {
_saveName.value = SuperString(oldValue, newValue, save)
fun onDatabaseChangeValidated() {
mDatabaseTaskProvider.onDatabaseChangeValidated()
}
fun saveDescription(oldValue: String,
newValue: String,
save: Boolean) {
_saveDescription.value = SuperString(oldValue, newValue, save)
/*
* Nodes actions
*/
fun createEntry(
newEntry: Entry,
parent: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseCreateEntry(
newEntry,
parent,
save
)
}
fun saveDefaultUsername(oldValue: String,
newValue: String,
save: Boolean) {
_saveDefaultUsername.value = SuperString(oldValue, newValue, save)
fun updateEntry(
oldEntry: Entry,
entryToUpdate: Entry,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseUpdateEntry(
oldEntry,
entryToUpdate,
save
)
}
fun saveColor(oldValue: String,
newValue: String,
save: Boolean) {
_saveColor.value = SuperString(oldValue, newValue, save)
fun restoreEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseRestoreEntryHistory(
mainEntryId,
entryHistoryPosition,
save
)
}
fun saveCompression(oldValue: CompressionAlgorithm,
newValue: CompressionAlgorithm,
save: Boolean) {
_saveCompression.value = SuperCompression(oldValue, newValue, save)
fun deleteEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int,
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) {
_removeUnlinkData.value = save
mDatabaseTaskProvider.startDatabaseRemoveUnlinkedData(save)
}
fun saveRecycleBin(oldValue: Group?,
newValue: Group?,
save: Boolean) {
_saveRecycleBin.value = SuperGroup(oldValue, newValue, save)
fun saveRecycleBin(
oldValue: Group?,
newValue: Group?,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveRecycleBin(
oldValue,
newValue,
save
)
}
fun saveTemplatesGroup(oldValue: Group?,
newValue: Group?,
save: Boolean) {
_saveTemplatesGroup.value = SuperGroup(oldValue, newValue, save)
fun saveTemplatesGroup(
oldValue: Group?,
newValue: Group?,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveTemplatesGroup(
oldValue,
newValue,
save
)
}
fun saveMaxHistoryItems(oldValue: Int,
newValue: Int,
save: Boolean) {
_saveMaxHistoryItems.value = SuperInt(oldValue, newValue, save)
fun saveMaxHistoryItems(
oldValue: Int,
newValue: Int,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMaxHistoryItems(
oldValue,
newValue,
save
)
}
fun saveMaxHistorySize(oldValue: Long,
newValue: Long,
save: Boolean) {
_saveMaxHistorySize.value = SuperLong(oldValue, newValue, save)
fun saveMaxHistorySize(
oldValue: Long,
newValue: Long,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMaxHistorySize(
oldValue,
newValue,
save
)
}
fun saveEncryption(oldValue: EncryptionAlgorithm,
newValue: EncryptionAlgorithm,
save: Boolean) {
_saveEncryption.value = SuperEncryption(oldValue, newValue, save)
fun saveEncryption(
oldValue: EncryptionAlgorithm,
newValue: EncryptionAlgorithm,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveEncryption(
oldValue,
newValue,
save
)
}
fun saveKeyDerivation(oldValue: KdfEngine,
newValue: KdfEngine,
save: Boolean) {
_saveKeyDerivation.value = SuperKeyDerivation(oldValue, newValue, save)
fun saveKeyDerivation(
oldValue: KdfEngine,
newValue: KdfEngine,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveKeyDerivation(
oldValue,
newValue,
save
)
}
fun saveIterations(oldValue: Long,
newValue: Long,
save: Boolean) {
_saveIterations.value = SuperLong(oldValue, newValue, save)
fun saveIterations(
oldValue: Long,
newValue: Long,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveIterations(
oldValue,
newValue,
save
)
}
fun saveMemoryUsage(oldValue: Long,
newValue: Long,
save: Boolean) {
_saveMemoryUsage.value = SuperLong(oldValue, newValue, save)
fun saveMemoryUsage(
oldValue: Long,
newValue: Long,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMemoryUsage(
oldValue,
newValue,
save
)
}
fun saveParallelism(oldValue: Long,
newValue: Long,
save: Boolean) {
_saveParallelism.value = SuperLong(oldValue, newValue, save)
fun saveParallelism(
oldValue: Long,
newValue: Long,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveParallelism(
oldValue,
newValue,
save
)
}
data class ActionResult(val database: ContextualDatabase,
val actionTask: String,
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)
/*
* Hardware Key
*/
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 androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
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.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.utils.IOActionTask
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
@@ -28,12 +30,18 @@ class EntryEditViewModel: NodeEditViewModel() {
private var mEntryId: NodeId<UUID>? = null
private var mParentId: NodeId<*>? = null
private var mRegisterInfo: RegisterInfo? = null
private var mSearchInfo: SearchInfo? = null
private var mParent: Group? = null
private var mEntry: Entry? = null
private var mIsTemplate: Boolean = false
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
private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
@@ -73,24 +81,28 @@ class EntryEditViewModel: NodeEditViewModel() {
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
fun loadDatabase(database: ContextualDatabase?) {
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo)
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun loadTemplateEntry(database: ContextualDatabase?) {
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
}
fun loadTemplateEntry(database: ContextualDatabase?,
entryId: NodeId<UUID>?,
parentId: NodeId<*>?,
registerInfo: RegisterInfo?,
searchInfo: SearchInfo?) {
fun loadTemplateEntry(
database: ContextualDatabase?,
entryId: NodeId<UUID>?,
parentId: NodeId<*>?,
registerInfo: RegisterInfo?
) {
this.mEntryId = entryId
this.mParentId = parentId
this.mRegisterInfo = registerInfo
this.mSearchInfo = searchInfo
database?.let {
mEntryId?.let {
IOActionTask(
{
scope = viewModelScope,
action = {
// Create an Entry copy to modify from the database entry
mEntry = database.getEntryById(it)
// Retrieve the parent
@@ -105,21 +117,24 @@ class EntryEditViewModel: NodeEditViewModel() {
database,
entry,
mIsTemplate,
registerInfo,
searchInfo
registerInfo
)
}
},
{ templatesEntry ->
onActionComplete = { templatesEntry ->
mEntryId = null
_templatesEntry.value = templatesEntry
if (templatesEntry?.overwrittenData == true) {
mUiState.value = UIState.ShowOverwriteMessage
}
}
).execute()
}
mParentId?.let {
IOActionTask(
{
scope = viewModelScope,
action = {
mParent = database.getGroupById(it)
mParent?.let { parentGroup ->
mEntry = database.createEntry()?.apply {
@@ -145,12 +160,11 @@ class EntryEditViewModel: NodeEditViewModel() {
database,
mEntry,
mIsTemplate,
registerInfo,
searchInfo
registerInfo
)
}
},
{ templatesEntry ->
onActionComplete = { templatesEntry ->
mParentId = null
_templatesEntry.value = templatesEntry
}
@@ -159,33 +173,37 @@ class EntryEditViewModel: NodeEditViewModel() {
}
}
private fun decodeTemplateEntry(database: ContextualDatabase,
entry: Entry?,
isTemplate: Boolean,
registerInfo: RegisterInfo?,
searchInfo: SearchInfo?): TemplatesEntry {
private fun decodeTemplateEntry(
database: ContextualDatabase,
entry: Entry?,
isTemplate: Boolean,
registerInfo: RegisterInfo?
): TemplatesEntry {
val templates = database.getTemplates(isTemplate)
val entryTemplate = entry?.let { database.getTemplate(it) }
?: Template.STANDARD
var entryInfo: EntryInfo? = null
var overwrittenData = false
// Decode the entry / load entry info
entry?.let {
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
// Load entry info
entry.getEntryInfo(database, true).let { tempEntryInfo ->
// Retrieve data from registration
// TODO only save registration
searchInfo?.let { tempSearchInfo ->
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
}
registerInfo?.let { regInfo ->
tempEntryInfo.saveRegisterInfo(database, regInfo)
overwrittenData = tempEntryInfo.saveRegisterInfo(database, regInfo)
}
entryInfo = tempEntryInfo
}
}
}
return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo)
return TemplatesEntry(
isTemplate,
templates,
entryTemplate,
entryInfo,
overwrittenData
)
}
fun changeTemplate(template: Template) {
@@ -198,44 +216,52 @@ class EntryEditViewModel: NodeEditViewModel() {
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
}
fun unlockAction() {
actionLocked = false
}
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
IOActionTask(
{
removeTempAttachmentsNotCompleted(entryInfo)
entry?.let { oldEntry ->
// Create a clone
var newEntry = Entry(oldEntry)
if (actionLocked.not()) {
actionLocked = true
IOActionTask(
scope = viewModelScope,
action = {
removeTempAttachmentsNotCompleted(entryInfo)
entry?.let { oldEntry ->
// Create a clone
var newEntry = Entry(oldEntry)
// Build info
newEntry.setEntryInfo(database, entryInfo)
// Build info
newEntry.setEntryInfo(database, entryInfo)
// Encode entry properties for template
_onTemplateChanged.value?.let { template ->
newEntry =
database?.encodeEntryWithTemplateConfiguration(newEntry, template)
?: newEntry
}
// Encode entry properties for template
_onTemplateChanged.value?.let { template ->
newEntry =
database?.encodeEntryWithTemplateConfiguration(newEntry, template)
?: newEntry
}
// Delete temp attachment if not used
mTempAttachments.forEach { tempAttachmentState ->
val tempAttachment = tempAttachmentState.attachment
database?.attachmentPool?.let { binaryPool ->
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
database.removeAttachmentIfNotUsed(tempAttachment)
// Delete temp attachment if not used
mTempAttachments.forEach { tempAttachmentState ->
val tempAttachment = tempAttachmentState.attachment
database?.attachmentPool?.let { binaryPool ->
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
database.removeAttachmentIfNotUsed(tempAttachment)
}
}
}
}
// Return entry to save
EntrySave(oldEntry, newEntry, parent)
// Return entry to save
EntrySave(oldEntry, newEntry, parent)
}
},
onActionComplete = { entrySave ->
entrySave?.let {
_onEntrySaved.value = it
}
}
},
{ entrySave ->
entrySave?.let {
_onEntrySaved.value = it
}
}
).execute()
).execute()
}
}
private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) {
@@ -322,10 +348,13 @@ class EntryEditViewModel: NodeEditViewModel() {
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
}
data class TemplatesEntry(val isTemplate: Boolean,
val templates: List<Template>,
val defaultTemplate: Template,
val entryInfo: EntryInfo?)
data class TemplatesEntry(
val isTemplate: Boolean,
val templates: List<Template>,
val defaultTemplate: Template,
val entryInfo: EntryInfo?,
val overwrittenData: Boolean = false
)
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 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 AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
sealed class UIState {
object Loading: UIState()
object ShowOverwriteMessage: UIState()
}
companion object {
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"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_entry_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true">

View File

@@ -101,6 +101,13 @@
android:checked="false"
style="@style/KeepassDXStyle.Chip.Filter"
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
android:id="@+id/search_chip_url"
android:layout_width="wrap_content"

View File

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

View File

@@ -485,7 +485,6 @@
<string name="search_mode">Axtarış modu</string>
<string name="save_mode">Yadda saxlama 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_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>

View File

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

View File

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

View File

@@ -209,7 +209,6 @@
<string name="search_mode">অনুসন্ধান মোড</string>
<string name="save_mode">সেভ মোড</string>
<string name="selection_mode">নির্বাচন মোড</string>
<string name="registration_mode">রেজিস্ট্রেশন মোড</string>
<string name="remember_keyfile_locations_summary">কী ফাইলগুলি কোথায় সংরক্ষণ করা হয় তা ট্র্যাক রাখে</string>
<string name="show_recent_files_title">সাম্প্রতিক ফাইল দেখান</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="style_brightness_summary">Escull tema clar o fosc</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="ask">Pregunta</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="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="registration_mode">Registrace</string>
<string name="save_mode">Režim ukládání</string>
<string name="search_mode">Vyhledávání</string>
<string name="error_field_name_already_exists">Jméno kolonky již existuje.</string>

View File

@@ -475,7 +475,6 @@
\n
\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="registration_mode">Registreringstilstand</string>
<string name="save_mode">Gem-tilstand</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>

View File

@@ -486,7 +486,6 @@
<string name="notification">Benachrichtigung</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="registration_mode">Registrierungsmodus</string>
<string name="save_mode">Speichermodus</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>

View File

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

View File

@@ -21,7 +21,7 @@
--><resources>
<string name="feedback">Comentarios</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="add_entry">Añadir apunte</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="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="registration_mode">Modo de registro</string>
<string name="save_mode">Modo de guardado</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>
@@ -690,4 +689,5 @@
<string name="generate_keyfile">Generar archivo de claves</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="error_otp_secret_length">La clave secreta debe tener al menos %1$d caracteres.</string>
</resources>

View File

@@ -383,7 +383,6 @@
<string name="search_mode">Otsinguviis</string>
<string name="save_mode">Salvestusviis</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="protection">Kaitse</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="save_mode">Gorde 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_summary">Erregistratu datu-baseen 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_title">وارد کردن ویژگی های برنامه</string>
<string name="export_app_properties_title">صادر کردن ویژگی های برنامه</string>
<string name="registration_mode">حالت ثبت</string>
<string name="save_mode">حالت ذخیره</string>
<string name="search_mode">حالت جستجو</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_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="registration_mode">Rekisteröintitila</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="description_app_properties">KeePassDX ominaisuudet sovellusasetusten hallintaan</string>

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