mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
302 Commits
4.2.0beta0
...
feature/Us
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c88413f7f7 | ||
|
|
7b1fb8a4bf | ||
|
|
3567fa797b | ||
|
|
eb41233e57 | ||
|
|
b394a99e40 | ||
|
|
2bbb40e513 | ||
|
|
09ef69e6ae | ||
|
|
762ac8f77b | ||
|
|
d28087d8d8 | ||
|
|
17d4c363ac | ||
|
|
c754b6a049 | ||
|
|
9c6241afc9 | ||
|
|
f6774b6d51 | ||
|
|
108a61905e | ||
|
|
d251788b1a | ||
|
|
7ed8a44168 | ||
|
|
844b1dfc79 | ||
|
|
d087fcc930 | ||
|
|
5fd25c6150 | ||
|
|
c1cfddddbe | ||
|
|
9146315001 | ||
|
|
609b536898 | ||
|
|
f9051ce787 | ||
|
|
d90d175bd8 | ||
|
|
c17fba8ef7 | ||
|
|
ed095ad0a7 | ||
|
|
82a8776911 | ||
|
|
753e9c4721 | ||
|
|
b64094ed20 | ||
|
|
bc854c63f7 | ||
|
|
3b793a72b8 | ||
|
|
f19afbdb2e | ||
|
|
622e9cefdd | ||
|
|
3ba56677ba | ||
|
|
39b4b4df70 | ||
|
|
4180ca92b0 | ||
|
|
bc9d00a1e1 | ||
|
|
5bdc72aa67 | ||
|
|
2be32e6884 | ||
|
|
612db4a6fc | ||
|
|
e74176f3bc | ||
|
|
af1fba42a0 | ||
|
|
bebf30aec1 | ||
|
|
321bb46df5 | ||
|
|
429f6db93f | ||
|
|
fc5a13160a | ||
|
|
c6eee8d449 | ||
|
|
7d227f372f | ||
|
|
3ac56b974f | ||
|
|
2e85ea401b | ||
|
|
fd080fb952 | ||
|
|
cc8e07366a | ||
|
|
c21bcbdbc2 | ||
|
|
e2ee17dae7 | ||
|
|
e68830fa25 | ||
|
|
9ddd66ce85 | ||
|
|
e3b69789bf | ||
|
|
54f2ed9fab | ||
|
|
2fea019b95 | ||
|
|
9ac7ef2d22 | ||
|
|
6d452fa49c | ||
|
|
d99edb6b4d | ||
|
|
cb679f0d59 | ||
|
|
2e237fba2d | ||
|
|
e68863a154 | ||
|
|
5dd9f75095 | ||
|
|
403021d38b | ||
|
|
fea7b30d6f | ||
|
|
ab5c859db4 | ||
|
|
3fcbc65de0 | ||
|
|
3f1ee6bbea | ||
|
|
37ce2ab781 | ||
|
|
ffaf4a761a | ||
|
|
56b7cc9118 | ||
|
|
987f3f9047 | ||
|
|
3039efc67c | ||
|
|
26daac4637 | ||
|
|
88a93829a9 | ||
|
|
7923a63d36 | ||
|
|
9a5c782d5d | ||
|
|
c39e4ba693 | ||
|
|
7db3d0502f | ||
|
|
d557e8b516 | ||
|
|
d6ae17657b | ||
|
|
3468b0f6f5 | ||
|
|
79777801e8 | ||
|
|
a202f66d48 | ||
|
|
ba58d5d47c | ||
|
|
46685592df | ||
|
|
ba9e2892ef | ||
|
|
a1da3b4fbd | ||
|
|
8bee0ec220 | ||
|
|
aebf6b21de | ||
|
|
0cf9253ea4 | ||
|
|
b63ceb37a4 | ||
|
|
c462dae6f5 | ||
|
|
ddf890b861 | ||
|
|
252eb30b13 | ||
|
|
62ab11cc56 | ||
|
|
e19ad3a8cc | ||
|
|
51fd8a77eb | ||
|
|
5ee0c2eb13 | ||
|
|
6d0ef8265c | ||
|
|
ea69d5acb2 | ||
|
|
1fb9595ec3 | ||
|
|
88e0bd51dc | ||
|
|
67477cc53b | ||
|
|
d2549d61d6 | ||
|
|
d6dc75961b | ||
|
|
f40c83812a | ||
|
|
b29c638d20 | ||
|
|
5bb03c2eef | ||
|
|
a76b1195e5 | ||
|
|
64da26f42c | ||
|
|
ef82552a0f | ||
|
|
d61b27ccd0 | ||
|
|
910ba99056 | ||
|
|
3de2a9acfd | ||
|
|
a48dccf27a | ||
|
|
2a561fb37e | ||
|
|
e27a329ac5 | ||
|
|
8e06a2a7cb | ||
|
|
ace82852af | ||
|
|
73369974b8 | ||
|
|
332eda8a7a | ||
|
|
e5ea1e35aa | ||
|
|
86aae9635a | ||
|
|
db3ccae87d | ||
|
|
4cec26967c | ||
|
|
a0368a4981 | ||
|
|
a1c7fe1e99 | ||
|
|
bf247ddeb7 | ||
|
|
1d2bc0fbfb | ||
|
|
85a12fe4ee | ||
|
|
a443ef996b | ||
|
|
c6995ad403 | ||
|
|
9018807eb8 | ||
|
|
b463106dd5 | ||
|
|
a23d28e1fa | ||
|
|
a0454f42d0 | ||
|
|
1c2ac88f47 | ||
|
|
11eb1bae45 | ||
|
|
089d86165a | ||
|
|
6a7362ad35 | ||
|
|
d2c10e2e4e | ||
|
|
0c20a14e67 | ||
|
|
acccf290de | ||
|
|
6ebe0f78af | ||
|
|
935c09ccd2 | ||
|
|
1eb10ad5bd | ||
|
|
ca4283151e | ||
|
|
8fb98ca4e7 | ||
|
|
be74c9710f | ||
|
|
24fb3c4c30 | ||
|
|
3bdc5fe600 | ||
|
|
c30884d6d0 | ||
|
|
5d26c3bd09 | ||
|
|
02e35cf5b7 | ||
|
|
085aefd2b9 | ||
|
|
1ea5b7a50c | ||
|
|
6cba96dd42 | ||
|
|
46238a76bc | ||
|
|
d5a9b664a1 | ||
|
|
6fdc4504d5 | ||
|
|
8a7c411a35 | ||
|
|
5ff9d5fa2f | ||
|
|
bb0f3c80d3 | ||
|
|
597d9c8274 | ||
|
|
4dd8c06fd2 | ||
|
|
72c66b3cd9 | ||
|
|
c2223afa6f | ||
|
|
d338d1340f | ||
|
|
ed4423666b | ||
|
|
d21fe662ff | ||
|
|
4da1c5bd92 | ||
|
|
18c18605fb | ||
|
|
988cb1a8d0 | ||
|
|
b6e01767e0 | ||
|
|
5414854e9c | ||
|
|
ae7f0732c6 | ||
|
|
d49d33fe3a | ||
|
|
5e7fc2d468 | ||
|
|
0d26e6a870 | ||
|
|
dd92f9ceb6 | ||
|
|
ff9239b9c4 | ||
|
|
319d35e485 | ||
|
|
28e65a4601 | ||
|
|
eb626e5bfe | ||
|
|
e1decf9a23 | ||
|
|
fff0e84b95 | ||
|
|
d73a7004b1 | ||
|
|
f71061e835 | ||
|
|
b2d25cc512 | ||
|
|
4d54b56c1d | ||
|
|
c764c6afff | ||
|
|
87b97a3849 | ||
|
|
5e6db44476 | ||
|
|
8615fa817f | ||
|
|
2f891bacd3 | ||
|
|
0d8a426df4 | ||
|
|
c952eb4415 | ||
|
|
2fd53b9416 | ||
|
|
244ca08890 | ||
|
|
208f1e97d5 | ||
|
|
e4e0628e20 | ||
|
|
f60f31771f | ||
|
|
ff6367bac4 | ||
|
|
540e72812e | ||
|
|
5fe4af8e9d | ||
|
|
ae42ab43b7 | ||
|
|
c463055971 | ||
|
|
1849dca81d | ||
|
|
b3dd3dcfb5 | ||
|
|
fef88ff270 | ||
|
|
f1f7dd1e6c | ||
|
|
409f290e33 | ||
|
|
96c3af097a | ||
|
|
4fe6b2e115 | ||
|
|
cc936b9304 | ||
|
|
e7f2a22583 | ||
|
|
4bf905ecda | ||
|
|
f8d80525d9 | ||
|
|
7ce6092270 | ||
|
|
65857596a6 | ||
|
|
e6253336bd | ||
|
|
e5595a3275 | ||
|
|
366e8bf1d7 | ||
|
|
fa63265599 | ||
|
|
755e0ea9a5 | ||
|
|
a819f2f8a8 | ||
|
|
c92da0a72f | ||
|
|
524963dbd8 | ||
|
|
50b1ac388e | ||
|
|
51c62034df | ||
|
|
e4d0cd89c6 | ||
|
|
bfe50fa985 | ||
|
|
3d798e6585 | ||
|
|
068c59ac98 | ||
|
|
34ec94a0c3 | ||
|
|
576a355342 | ||
|
|
aa19f11699 | ||
|
|
2fb4dff46d | ||
|
|
e6cf3f12a5 | ||
|
|
ca94ce86ba | ||
|
|
dea6b25bb4 | ||
|
|
c48f64d331 | ||
|
|
5e3a504c1f | ||
|
|
b9b7d7b2db | ||
|
|
e085d5d277 | ||
|
|
05336e93a0 | ||
|
|
90b3b56893 | ||
|
|
02c514272e | ||
|
|
989e47ed12 | ||
|
|
1caf132558 | ||
|
|
1b98bd740c | ||
|
|
5adeb5cde0 | ||
|
|
b949d5d861 | ||
|
|
b4264a30a4 | ||
|
|
cf799c0f68 | ||
|
|
97f0ca519b | ||
|
|
cf4047b701 | ||
|
|
40608a3eb5 | ||
|
|
99cb50d031 | ||
|
|
b0d0c35241 | ||
|
|
6044c93a4a | ||
|
|
b544b5d54d | ||
|
|
852378e484 | ||
|
|
711a344860 | ||
|
|
72087c7e5c | ||
|
|
a337de3679 | ||
|
|
75b37f5a9f | ||
|
|
075f54b286 | ||
|
|
e07cbc2e14 | ||
|
|
ac29b7bac7 | ||
|
|
b9129cb941 | ||
|
|
6957fcd81a | ||
|
|
cfe56fc055 | ||
|
|
6f3e065ad1 | ||
|
|
abfa7a3f47 | ||
|
|
dd0d85e54e | ||
|
|
76c20263f7 | ||
|
|
e447388611 | ||
|
|
1bfec67c02 | ||
|
|
45041216d6 | ||
|
|
e075e9018c | ||
|
|
eed304ec40 | ||
|
|
5bcbbac97f | ||
|
|
ea4750fc11 | ||
|
|
5037821529 | ||
|
|
3a4c88f19a | ||
|
|
e960a8e169 | ||
|
|
1d4e1687cf | ||
|
|
033fa95285 | ||
|
|
f17d211fbd | ||
|
|
ae903ad236 | ||
|
|
7c3a15ce79 | ||
|
|
b609d4e182 | ||
|
|
e8ecf28f7c | ||
|
|
3d5adbfc01 | ||
|
|
72bfc50703 | ||
|
|
a60e2e780d | ||
|
|
9210851765 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Bug Report
|
||||
name: Bug report
|
||||
description: Report a bug.
|
||||
labels: ["bug"]
|
||||
body:
|
||||
|
||||
45
CHANGELOG
45
CHANGELOG
@@ -1,10 +1,45 @@
|
||||
KeePassDX(4.3.0)
|
||||
* Manual change of app language #1884 #1990
|
||||
* Add Passkey User Verification #2283
|
||||
* Fix autofill username detection #2276
|
||||
* Fix Passkey in passwordless mode #2282
|
||||
|
||||
KeePassDX(4.2.4)
|
||||
* Fix remembering database location #2262
|
||||
|
||||
KeePassDX(4.2.3)
|
||||
* Fix multiple Passkey selection #2253
|
||||
* Fix database dialog subtitle #2254
|
||||
* Fix save search info if URL present #2255
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.2)
|
||||
* Fix database merge algorithm #2223
|
||||
* Fix save search info #2243
|
||||
* Fix Play Service as privileged app for Passkey Cross Device Authentication #2244
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.1)
|
||||
* Fix Magikeyboard autosearch #2233
|
||||
* Fix database merge #2223
|
||||
* Fix dialog database action #2234
|
||||
* Fix autofill selection #2238 #2235
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.0)
|
||||
* Passkeys management #1421 #2097 (Thx @cali-95)
|
||||
* Confirm usage of passkey #2165
|
||||
* Passkeys management #1421 #2097 (@cali-95)
|
||||
* Confirm usage of passkey #2165 #2124
|
||||
* Dialog to manage missing signature #2152 #2155 #2161 #2160
|
||||
* Capture error #2159
|
||||
* Change Passkey Backup Eligibility & Backup State #2135 #2150
|
||||
* Small fixes #2157 #2164
|
||||
* 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)
|
||||
|
||||
22
README.md
22
README.md
@@ -6,19 +6,21 @@
|
||||
|
||||
### Features
|
||||
|
||||
- Create database files / entries and groups.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||
- **Passkeys** for authentication and **local storage of private keys**.
|
||||
- **Biometric recognition** for fast unlocking (fingerprint / face unlock / …).
|
||||
- **One-Time Password** management (HOTP / TOTP) for two-factor authentication (2FA).
|
||||
- **Autofill** for easy form filling with passwords.
|
||||
- **Magikeyboard** to efficiently fill in any field.
|
||||
- Create **encrypted database files**.
|
||||
- Organisation of credentials by **entry** and in **group** trees.
|
||||
- Allows opening and **copying URI / URL fields quickly**.
|
||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||
- Material design with **themes**.
|
||||
- **Auto-Fill** and Integration.
|
||||
- Field filling **keyboard**.
|
||||
- Dynamic **templates**
|
||||
- Dynamic **templates** for each type of entry.
|
||||
- **History** of each entry.
|
||||
- Precise management of **settings**.
|
||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||
- Material design with **themes**.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||
- Code written in **native languages** (Kotlin / Java / JNI / C).
|
||||
|
||||
KeePassDX is **open source** and **ad-free**.
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 35
|
||||
versionCode = 142
|
||||
versionName = "4.2.0beta02"
|
||||
versionCode = 150
|
||||
versionName = "4.3.0"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -110,6 +110,10 @@ android {
|
||||
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
|
||||
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig true
|
||||
}
|
||||
}
|
||||
|
||||
def room_version = "2.5.1"
|
||||
|
||||
@@ -48,6 +48,18 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.ironfoxoss.ironfox.nightly",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -41,6 +41,9 @@ import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
@@ -53,6 +56,11 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.requestShowUnprotectField
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
@@ -78,12 +86,17 @@ import com.kunzisoft.keepass.view.changeTitleColor
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.view.showError
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
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
|
||||
@@ -98,14 +111,10 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
private var loadingView: ProgressBar? = null
|
||||
|
||||
private val mEntryViewModel: EntryViewModel by viewModels()
|
||||
private val mUserVerificationViewModel: UserVerificationViewModel by viewModels()
|
||||
|
||||
private val mEntryActivityEducation = EntryActivityEducation(this)
|
||||
|
||||
private var mMainEntryId: NodeId<UUID>? = null
|
||||
private var mHistoryPosition: Int = -1
|
||||
private var mEntryIsHistory: Boolean = false
|
||||
private var mEntryLoaded = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
private var mAttachmentSelected: Attachment? = null
|
||||
@@ -123,6 +132,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
private var mBackgroundColor: Int? = null
|
||||
private var mForegroundColor: Int? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -135,6 +146,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 +162,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
|
||||
@@ -201,7 +217,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
} catch (_: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
@@ -229,13 +245,9 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
this.mMainEntryId = entryInfoHistory.mainEntryId
|
||||
|
||||
// Manage history position
|
||||
val historyPosition = entryInfoHistory.historyPosition
|
||||
this.mHistoryPosition = historyPosition
|
||||
val entryIsHistory = historyPosition > -1
|
||||
this.mEntryIsHistory = entryIsHistory
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||
// TODO History badge
|
||||
@@ -270,7 +282,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
mForegroundColor = if (showEntryColors) entryInfo.foregroundColor else null
|
||||
|
||||
loadingView?.hideByFading()
|
||||
mEntryLoaded = true
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
@@ -305,14 +316,81 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mEntryViewModel.entryState.collect { entryState ->
|
||||
when (entryState) {
|
||||
is EntryViewModel.EntryState.Loading -> {}
|
||||
is EntryViewModel.EntryState.RequestUnprotectField -> {
|
||||
mDatabase?.let { database ->
|
||||
requestShowUnprotectField(
|
||||
userVerificationViewModel = mUserVerificationViewModel,
|
||||
database = database,
|
||||
protectedFieldView = entryState.protectedFieldView
|
||||
)
|
||||
}
|
||||
mEntryViewModel.actionPerformed()
|
||||
}
|
||||
is EntryViewModel.EntryState.RequestCopyProtectedField -> {
|
||||
mDatabase?.let { database ->
|
||||
checkUserVerification(
|
||||
userVerificationViewModel = mUserVerificationViewModel,
|
||||
dataToVerify = UserVerificationData(
|
||||
actionType = UserVerificationActionType.COPY_PROTECTED_FIELD,
|
||||
database = database,
|
||||
field = entryState.field,
|
||||
)
|
||||
)
|
||||
}
|
||||
mEntryViewModel.actionPerformed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mUserVerificationViewModel.userVerificationState.collect { uVState ->
|
||||
when (uVState) {
|
||||
is UserVerificationViewModel.UVState.Loading -> {}
|
||||
is UserVerificationViewModel.UVState.OnUserVerificationCanceled -> {
|
||||
coordinatorLayout?.showError(uVState.error)
|
||||
mUserVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
is UserVerificationViewModel.UVState.OnUserVerificationSucceeded -> {
|
||||
val data = uVState.dataToVerify
|
||||
when (data.actionType) {
|
||||
UserVerificationActionType.SHOW_PROTECTED_FIELD -> {
|
||||
// Unprotect field by its view
|
||||
data.protectedFieldView?.unprotect()
|
||||
}
|
||||
UserVerificationActionType.COPY_PROTECTED_FIELD -> {
|
||||
// Copy field value
|
||||
data.field?.let {
|
||||
mEntryViewModel.copyToClipboard(it)
|
||||
}
|
||||
}
|
||||
UserVerificationActionType.EDIT_ENTRY -> {
|
||||
// Edit Entry
|
||||
editEntry(data.database, data.entryId)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
mUserVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
@@ -323,9 +401,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
return coordinatorLayout
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mEntryViewModel.loadDatabase(database)
|
||||
}
|
||||
|
||||
@@ -402,13 +479,13 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
if (mEntryLoaded) {
|
||||
if (mEntryViewModel.entryLoaded) {
|
||||
val inflater = menuInflater
|
||||
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
|
||||
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
||||
if (mEntryViewModel.entryIsHistory && !mDatabaseReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
}
|
||||
|
||||
@@ -421,7 +498,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||
if (mEntryViewModel.entryIsHistory || mDatabaseReadOnly) {
|
||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
||||
@@ -466,33 +543,53 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun editEntry(database: ContextualDatabase?, entryId: NodeId<*>?) {
|
||||
database?.let { database ->
|
||||
entryId?.let { entryId ->
|
||||
EntryEditActivity.launch(
|
||||
activity = this@EntryActivity,
|
||||
database = database,
|
||||
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||
nodeId = entryId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_edit -> {
|
||||
if (mEntryViewModel.entryInfo?.isUserVerificationNeeded() == true) {
|
||||
mDatabase?.let { database ->
|
||||
mMainEntryId?.let { entryId ->
|
||||
EntryEditActivity.launchToUpdate(
|
||||
this,
|
||||
database,
|
||||
entryId,
|
||||
mEntryActivityResultLauncher
|
||||
checkUserVerification(
|
||||
userVerificationViewModel = mUserVerificationViewModel,
|
||||
dataToVerify = UserVerificationData(
|
||||
actionType = UserVerificationActionType.EDIT_ENTRY,
|
||||
database = database,
|
||||
entryId = mEntryViewModel.mainEntryId
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
editEntry(mDatabase, mEntryViewModel.mainEntryId)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.menu_restore_entry_history -> {
|
||||
mMainEntryId?.let { mainEntryId ->
|
||||
mEntryViewModel.mainEntryId?.let { mainEntryId ->
|
||||
restoreEntryHistory(
|
||||
mainEntryId,
|
||||
mHistoryPosition)
|
||||
mEntryViewModel.historyPosition
|
||||
)
|
||||
}
|
||||
}
|
||||
R.id.menu_delete_entry_history -> {
|
||||
mMainEntryId?.let { mainEntryId ->
|
||||
mEntryViewModel.mainEntryId?.let { mainEntryId ->
|
||||
deleteEntryHistory(
|
||||
mainEntryId,
|
||||
mHistoryPosition)
|
||||
mEntryViewModel.historyPosition
|
||||
)
|
||||
}
|
||||
}
|
||||
R.id.menu_save_database -> {
|
||||
@@ -512,8 +609,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
override fun finish() {
|
||||
// Transit data in previous Activity after an update
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||
setResult(Activity.RESULT_OK, this)
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntryViewModel.mainEntryId)
|
||||
setResult(RESULT_OK, this)
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
@@ -527,34 +624,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,
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
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>) {
|
||||
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)
|
||||
historyPosition?.let {
|
||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||
}
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 +59,12 @@ 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.UserVerificationActionType
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.requestShowUnprotectField
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
@@ -98,9 +100,13 @@ import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.view.showError
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.EnumSet
|
||||
import java.util.UUID
|
||||
|
||||
class EntryEditActivity : DatabaseLockActivity(),
|
||||
@@ -127,6 +133,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
|
||||
|
||||
private val mColorPickerViewModel: ColorPickerViewModel by viewModels()
|
||||
private val mUserVerificationViewModel: UserVerificationViewModel by viewModels()
|
||||
|
||||
private var mAllowCustomFields = false
|
||||
private var mAllowOTP = false
|
||||
@@ -155,8 +162,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
// To ask data lost only one time
|
||||
private var backPressedAlreadyApproved = false
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -181,8 +187,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 +216,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
mDatabase,
|
||||
entryId,
|
||||
parentId,
|
||||
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
|
||||
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
intent.retrieveRegisterInfo()
|
||||
?: intent.retrieveSearchInfo()?.toRegisterInfo()
|
||||
)
|
||||
|
||||
// To retrieve attachment
|
||||
@@ -374,31 +384,60 @@ 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.entryEditState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is EntryEditViewModel.EntryEditState.Loading -> {}
|
||||
is EntryEditViewModel.EntryEditState.ShowOverwriteMessage -> {
|
||||
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) { _, _ -> }
|
||||
.create().show()
|
||||
mEntryEditViewModel.actionPerformed()
|
||||
}
|
||||
is EntryEditViewModel.EntryEditState.RequestUnprotectField -> {
|
||||
mDatabase?.let { database ->
|
||||
requestShowUnprotectField(
|
||||
userVerificationViewModel = mUserVerificationViewModel,
|
||||
database = database,
|
||||
protectedFieldView = uiState.protectedFieldView
|
||||
)
|
||||
}
|
||||
mEntryEditViewModel.actionPerformed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mUserVerificationViewModel.userVerificationState.collect { uVState ->
|
||||
when (uVState) {
|
||||
is UserVerificationViewModel.UVState.Loading -> {}
|
||||
is UserVerificationViewModel.UVState.OnUserVerificationCanceled -> {
|
||||
coordinatorLayout?.showError(uVState.error)
|
||||
mUserVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
is UserVerificationViewModel.UVState.OnUserVerificationSucceeded -> {
|
||||
when (uVState.dataToVerify.actionType) {
|
||||
UserVerificationActionType.SHOW_PROTECTED_FIELD -> {
|
||||
uVState.dataToVerify.protectedFieldView?.unprotect()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
mUserVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,13 +449,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 +466,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 +482,27 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
searchAction = {
|
||||
// Nothing when search retrieved
|
||||
},
|
||||
saveAction = {
|
||||
entryValidatedForSave(entry)
|
||||
},
|
||||
keyboardSelectionAction = {
|
||||
selectionAction = { _, typeMode, _ ->
|
||||
when(typeMode) {
|
||||
TypeMode.DEFAULT -> {}
|
||||
TypeMode.MAGIKEYBOARD ->
|
||||
entryValidatedForKeyboardSelection(database, entry)
|
||||
TypeMode.PASSKEY ->
|
||||
entryValidatedForPasskey(database, entry)
|
||||
TypeMode.AUTOFILL ->
|
||||
entryValidatedForAutofill(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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -477,46 +521,26 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
|
||||
// Populate Magikeyboard with entry
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entry.getEntryInfo(database)
|
||||
// Build Magikeyboard response with the entry selected
|
||||
this.buildSpecialModeResponseAndSetResult(
|
||||
entryInfo = entry.getEntryInfo(database),
|
||||
extras = buildEntryResult(entry)
|
||||
)
|
||||
onValidateSpecialMode()
|
||||
// Don't keep activity history for entry edition
|
||||
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 +781,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 +807,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 +815,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
enum class RegistrationType {
|
||||
UPDATE, CREATE
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = EntryEditActivity::class.java.name
|
||||
@@ -800,23 +828,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 +844,72 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
||||
* Launch EntryEditActivity to update an existing entry or to add a new entry in an existing group
|
||||
*/
|
||||
fun launchToUpdate(activity: Activity,
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
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,
|
||||
fun launchForSelection(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
typeMode: TypeMode,
|
||||
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) {
|
||||
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,
|
||||
fun launchForRegistration(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
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.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<*>,
|
||||
nodeId: NodeId<*>,
|
||||
registerInfo: RegisterInfo? = null,
|
||||
typeMode: TypeMode) {
|
||||
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)
|
||||
when (registrationType) {
|
||||
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||
}
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context,
|
||||
activityResultLauncher,
|
||||
|
||||
@@ -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,20 +47,16 @@ 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
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||
@@ -99,8 +92,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.buildActivityResultLauncher()
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
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
|
||||
@@ -222,39 +214,24 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
// Retrieve settings for default database
|
||||
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||
}
|
||||
|
||||
// Remove all the remember locations if needed
|
||||
if (PreferencesUtil.rememberDatabaseLocations(applicationContext).not()) {
|
||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
.deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
if (database != null) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Update list
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK,
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
result.data?.getParcelableCompat<Uri>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||
val mainCredential =
|
||||
result.data?.getParcelableCompat(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
||||
?: MainCredential()
|
||||
databaseFilesViewModel.addDatabaseFile(
|
||||
databaseUri,
|
||||
mainCredential.keyFileUri,
|
||||
mainCredential.hardwareKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Launch activity
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK -> {
|
||||
@@ -263,13 +240,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
database,
|
||||
false
|
||||
)
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,17 +264,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 +325,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 +355,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
mDatabase?.let { database ->
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
|
||||
// Show recent files if allowed
|
||||
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||
databaseFilesViewModel.loadListOfDatabases()
|
||||
@@ -358,7 +373,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 +457,35 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSearchResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
fun launchForSearch(
|
||||
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,
|
||||
fun launchForSelection(
|
||||
context: Context,
|
||||
typeMode: TypeMode,
|
||||
searchInfo: SearchInfo? = null,
|
||||
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
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo = searchInfo,
|
||||
typeMode = typeMode,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
|
||||
@@ -515,16 +494,18 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
fun launchForRegistration(
|
||||
context: Context,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo? = null,
|
||||
typeMode: TypeMode) {
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context,
|
||||
activityResultLauncher,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo,
|
||||
typeMode
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo = registerInfo,
|
||||
typeMode = typeMode,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,8 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -174,10 +176,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
|
||||
|
||||
@@ -45,6 +45,8 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
private lateinit var imageView: ImageView
|
||||
private lateinit var progressView: View
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -101,7 +103,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
try {
|
||||
@@ -119,7 +121,6 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
resources.displayMetrics.heightPixels * 2
|
||||
)
|
||||
|
||||
database?.let { database ->
|
||||
BinaryDatabaseManager.loadBitmap(
|
||||
database,
|
||||
attachment.binaryData,
|
||||
@@ -132,7 +133,6 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
imageView.setImageBitmap(bitmapLoaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to view the binary", e)
|
||||
|
||||
@@ -28,6 +28,8 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
|
||||
private lateinit var validationButton: View
|
||||
private var lockView: View? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -36,8 +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
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
@@ -56,10 +54,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,8 +124,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
private var mReadOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
|
||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.buildActivityResultLauncher()
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -310,15 +305,10 @@ 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
|
||||
@@ -330,7 +320,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
}
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
@@ -514,10 +503,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 +562,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 +586,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUUID: Boolean) {
|
||||
loadDatabase(
|
||||
mDatabaseViewModel.loadDatabase(
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
@@ -752,11 +739,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity,
|
||||
private fun buildAndLaunchIntent(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
intentBuildLauncher: (Intent) -> Unit
|
||||
) {
|
||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||
if (keyFile != null)
|
||||
@@ -773,10 +762,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(activity: Activity,
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
hardwareKey: HardwareKey?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
@@ -789,103 +780,45 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSearchResult(activity: Activity,
|
||||
fun launchForSearchResult(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
context = activity,
|
||||
intent = intent,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Save Launch
|
||||
* Selection Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSaveResult(activity: Activity,
|
||||
fun launchForSelection(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
typeMode: TypeMode,
|
||||
searchInfo: SearchInfo?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
) {
|
||||
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
|
||||
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||
context = activity,
|
||||
intent = intent,
|
||||
typeMode = typeMode,
|
||||
searchInfo = searchInfo,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -895,139 +828,25 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForRegistration(
|
||||
activity: Activity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo?
|
||||
registerInfo: RegisterInfo?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context = activity,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
intent = intent,
|
||||
typeMode = typeMode,
|
||||
registerInfo = registerInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* 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,
|
||||
registerInfo = registerInfo,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.MasterCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
|
||||
|
||||
|
||||
class CheckDatabaseCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private val userVerificationViewModel: UserVerificationViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
val inflater = activity.layoutInflater
|
||||
val rootView = inflater.inflate(R.layout.fragment_check_database_credential, null)
|
||||
val editText = rootView.findViewById<TextView>(R.id.setup_check_password_edit_text)
|
||||
editText.filters = arrayOf<InputFilter>(InputFilter.LengthFilter(
|
||||
MasterCredential.CHECK_KEY_PASSWORD_LENGTH)
|
||||
)
|
||||
builder.setView(rootView)
|
||||
.setPositiveButton(R.string.check) { _, _ ->
|
||||
userVerificationViewModel.checkMainCredential(
|
||||
editText.text.toString()
|
||||
)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
userVerificationViewModel.onUserVerificationFailed()
|
||||
dismiss()
|
||||
}
|
||||
rootView.findViewById<View>(R.id.user_verification_information)?.setOnClickListener {
|
||||
activity.openUrl(R.string.user_verification_explanation_url)
|
||||
}
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun getInstance(): CheckDatabaseCredentialDialogFragment {
|
||||
val fragment = CheckDatabaseCredentialDialogFragment()
|
||||
val args = Bundle()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,7 +86,8 @@ 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,
|
||||
fun getInstance(
|
||||
oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnly: Boolean
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
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)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -20,45 +20,27 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.view.MainCredentialView
|
||||
import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel
|
||||
|
||||
class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mainCredentialView: MainCredentialView? = null
|
||||
|
||||
private var mListener: AskMainCredentialDialogListener? = null
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
interface AskMainCredentialDialogListener {
|
||||
fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||
fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Context) {
|
||||
super.onAttach(activity)
|
||||
try {
|
||||
mListener = activity as AskMainCredentialDialogListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(activity.toString()
|
||||
+ " must implement " + AskMainCredentialDialogListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
private val mMainCredentialViewModel: MainCredentialViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
@@ -76,23 +58,21 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
databaseUri?.let {
|
||||
root.findViewById<TextView>(R.id.title_database)?.text =
|
||||
it.getDocumentFile(requireContext())?.name
|
||||
}
|
||||
|
||||
builder.setView(root)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onAskMainCredentialDialogPositiveClick(
|
||||
databaseUri,
|
||||
retrieveMainCredential()
|
||||
mMainCredentialViewModel.validateMainCredential(
|
||||
databaseUri = databaseUri,
|
||||
mainCredential = retrieveMainCredential()
|
||||
)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
mListener?.onAskMainCredentialDialogNegativeClick(
|
||||
databaseUri,
|
||||
retrieveMainCredential()
|
||||
mMainCredentialViewModel.cancelMainCredential(
|
||||
databaseUri = databaseUri
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
@@ -100,6 +80,13 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
} ?: run {
|
||||
mMainCredentialViewModel.cancelMainCredential(
|
||||
databaseUri = null,
|
||||
error = FileNotFoundDatabaseException()
|
||||
)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
|
||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,9 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
setOnForegroundColorClickListener {
|
||||
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
|
||||
}
|
||||
setOnUnprotectClickListener { _, textEditFieldView ->
|
||||
mEntryEditViewModel.requestUnprotectField(textEditFieldView)
|
||||
}
|
||||
setOnCustomEditionActionClickListener { field ->
|
||||
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||
}
|
||||
@@ -230,7 +233,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 +276,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 +293,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?) {
|
||||
|
||||
@@ -16,13 +16,10 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.view.TemplateView
|
||||
@@ -50,8 +47,6 @@ class EntryFragment: DatabaseFragment() {
|
||||
private lateinit var uuidContainerView: View
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
private var mClipboardHelper: ClipboardHelper? = null
|
||||
|
||||
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
@@ -66,10 +61,6 @@ class EntryFragment: DatabaseFragment() {
|
||||
savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
context?.let { context ->
|
||||
mClipboardHelper = ClipboardHelper(context)
|
||||
}
|
||||
|
||||
rootView = view
|
||||
// Hide only the first time
|
||||
if (savedInstanceState == null) {
|
||||
@@ -133,7 +124,7 @@ class EntryFragment: DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
context?.let { context ->
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||
attachmentsAdapter?.database = database
|
||||
@@ -152,16 +143,14 @@ class EntryFragment: DatabaseFragment() {
|
||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||
// Set copy buttons
|
||||
templateView.apply {
|
||||
setOnUnprotectClickListener { protectedFieldView ->
|
||||
mEntryViewModel.requestUnprotectField(protectedFieldView)
|
||||
}
|
||||
setOnAskCopySafeClickListener {
|
||||
showClipboardDialog()
|
||||
}
|
||||
|
||||
setOnCopyActionClickListener { field ->
|
||||
mClipboardHelper?.timeoutCopyToClipboard(
|
||||
TemplateField.getLocalizedName(context, field.name),
|
||||
field.protectedValue.stringValue,
|
||||
field.protectedValue.isProtected
|
||||
)
|
||||
setOnCopyActionClickListener { field, protectedFieldView ->
|
||||
mEntryViewModel.requestCopyField(field, protectedFieldView)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,14 +231,14 @@ class EntryFragment: DatabaseFragment() {
|
||||
fun firstEntryFieldCopyView(): View? {
|
||||
return try {
|
||||
templateView.getActionImageView()
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun launchEntryCopyEducationAction() {
|
||||
val appNameString = getString(R.string.app_name)
|
||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString, appNameString)
|
||||
mEntryViewModel.copyToClipboard(appNameString)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -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,9 +154,8 @@ 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) {
|
||||
@@ -195,7 +194,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
mNodesRecyclerView?.adapter = mAdapter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,104 +1,307 @@
|
||||
package com.kunzisoft.keepass.activities.legacy
|
||||
|
||||
import android.net.Uri
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
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.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||
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.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.tasks.ProgressTaskViewModel
|
||||
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 val progressTaskViewModel: ProgressTaskViewModel by viewModels()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful to only waiting for the activity result and prevent any parallel action
|
||||
*/
|
||||
var credentialResultLaunched = false
|
||||
|
||||
/**
|
||||
* Utility activity result launcher,
|
||||
* Used recursively, close each activity with return data
|
||||
*/
|
||||
protected var mCredentialActivityResultLauncher: CredentialActivityResultLauncher =
|
||||
CredentialActivityResultLauncher(
|
||||
registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
setActivityResult(
|
||||
lockDatabase = false,
|
||||
resultCode = it.resultCode,
|
||||
data = it.data
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Custom ActivityResultLauncher to manage the database action
|
||||
*/
|
||||
protected inner class CredentialActivityResultLauncher(
|
||||
val builder: ActivityResultLauncher<Intent>
|
||||
) : ActivityResultLauncher<Intent>() {
|
||||
|
||||
override fun launch(
|
||||
input: Intent?,
|
||||
options: ActivityOptionsCompat?
|
||||
) {
|
||||
credentialResultLaunched = true
|
||||
builder.launch(input, options)
|
||||
}
|
||||
|
||||
override fun unregister() {
|
||||
builder.unregister()
|
||||
}
|
||||
|
||||
override fun getContract(): ActivityResultContract<Intent?, *> {
|
||||
return builder.getContract()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(CREDENTIAL_RESULT_LAUNCHER_KEY)
|
||||
) {
|
||||
credentialResultLaunched = savedInstanceState.getBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY)
|
||||
}
|
||||
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
val databaseWasReloaded = database?.wasReloaded == true
|
||||
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
if (credentialResultLaunched.not()) {
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.Wait -> {}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
|
||||
if (finishActivityIfReloadRequested()) {
|
||||
finish()
|
||||
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
|
||||
database?.wasReloaded = false
|
||||
}
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> {
|
||||
if (manageDatabaseInfo()) {
|
||||
showDatabaseChangedDialog(
|
||||
uiState.previousDatabaseInfo,
|
||||
uiState.newDatabaseInfo,
|
||||
uiState.readOnlyDatabase
|
||||
)
|
||||
}
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
|
||||
startDatabasePermissionService(
|
||||
uiState.bundle,
|
||||
uiState.actionTask
|
||||
)
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
|
||||
progressTaskViewModel.show(uiState.progressMessage)
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
|
||||
progressTaskViewModel.show(uiState.progressMessage)
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
|
||||
progressTaskViewModel.hide()
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
progressTaskViewModel.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
progressTaskViewModel.progressTaskState.collect { state ->
|
||||
when (state) {
|
||||
is ProgressTaskViewModel.ProgressTaskState.Show ->
|
||||
startDialog()
|
||||
is ProgressTaskViewModel.ProgressTaskState.Hide ->
|
||||
stopDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
if (credentialResultLaunched.not()) {
|
||||
// Nullable function
|
||||
onUnknownDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
|
||||
onDatabaseActionFinished(database, actionTask, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun showDatabaseDialog(): Boolean {
|
||||
return true
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY, credentialResultLaunched)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
) {
|
||||
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDatabase(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUuid: Boolean
|
||||
private fun showDatabaseChangedDialog(
|
||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnlyDatabase: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider?.startDatabaseLoad(
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
fixDuplicateUuid
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
protected fun closeDatabase() {
|
||||
mDatabase?.clearAndClose(this.getBinaryDir())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
mDatabaseTaskProvider?.registerProgressTask()
|
||||
private fun startDialog() {
|
||||
lifecycleScope.launch {
|
||||
if (showDatabaseDialog()) {
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = supportFragmentManager
|
||||
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||
progressTaskDialogFragment?.show(
|
||||
supportFragmentManager,
|
||||
PROGRESS_TASK_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||
super.onPause()
|
||||
private fun stopDialog() {
|
||||
progressTaskDialogFragment?.dismissAllowingStateLoss()
|
||||
progressTaskDialogFragment = null
|
||||
}
|
||||
|
||||
protected open fun showDatabaseDialog(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CREDENTIAL_RESULT_LAUNCHER_KEY = "com.kunzisoft.keepass.CREDENTIAL_RESULT_LAUNCHER_KEY"
|
||||
}
|
||||
}
|
||||
@@ -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,106 +87,23 @@ 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()
|
||||
@@ -209,7 +126,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
|
||||
checkRegister()
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
// To fix weird crash
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -450,9 +370,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.lock) { _, _ ->
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
finish()
|
||||
}.create().show()
|
||||
} else {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,13 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
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.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
@@ -21,7 +26,7 @@ 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
|
||||
|
||||
@@ -50,8 +55,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
|
||||
fun onLaunchActivitySpecialMode() {
|
||||
if (!isIntentSender()) {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -60,8 +65,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 +78,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 +110,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 +130,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 +149,6 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
visible = when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> false
|
||||
SpecialMode.SEARCH -> true
|
||||
SpecialMode.SAVE -> true
|
||||
SpecialMode.SELECTION -> true
|
||||
SpecialMode.REGISTRATION -> true
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
fun onDatabaseRetrieved(database: ContextualDatabase)
|
||||
|
||||
fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result)
|
||||
result: ActionRunnable.Result
|
||||
)
|
||||
}
|
||||
@@ -24,26 +24,31 @@ 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.database.element.node.NodeIdUUID
|
||||
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.getEnum
|
||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableList
|
||||
import com.kunzisoft.keepass.utils.putEnum
|
||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||
import com.kunzisoft.keepass.utils.putParcelableList
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
|
||||
object EntrySelectionHelper {
|
||||
|
||||
@@ -51,6 +56,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 +65,7 @@ object EntrySelectionHelper {
|
||||
fun Activity.setActivityResult(
|
||||
lockDatabase: Boolean = false,
|
||||
resultCode: Int,
|
||||
data: Intent? = null,
|
||||
data: Intent? = null
|
||||
) {
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK ->
|
||||
@@ -68,170 +75,212 @@ 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) {
|
||||
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(
|
||||
fun startActivityForSearchModeResult(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
||||
intent.addAutofillComponent(context, autofillComponent)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
activityResultLauncher?.launch(intent)
|
||||
intent.addSpecialMode(SpecialMode.SEARCH)
|
||||
intent.addSearchInfo(searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
fun startActivityForPasskeySelectionModeResult(
|
||||
fun startActivityForSelectionModeResult(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
searchInfo: SearchInfo?
|
||||
typeMode: TypeMode,
|
||||
searchInfo: SearchInfo?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||
) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
addTypeModeInIntent(intent, TypeMode.PASSKEY)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
activityResultLauncher?.launch(intent)
|
||||
intent.addSpecialMode(SpecialMode.SELECTION)
|
||||
intent.addTypeMode(typeMode)
|
||||
intent.addSearchInfo(searchInfo)
|
||||
if (activityResultLauncher == null) {
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun 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 Bundle.addSearchInfo(searchInfo: SearchInfo?): Bundle {
|
||||
searchInfo?.let {
|
||||
putParcelable(KEY_SEARCH_INFO, it)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
||||
return getParcelableExtraCompat(KEY_SEARCH_INFO)
|
||||
}
|
||||
|
||||
fun Bundle.getSearchInfo(): SearchInfo? {
|
||||
return getParcelableCompat(KEY_SEARCH_INFO)
|
||||
}
|
||||
|
||||
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 Bundle.addRegisterInfo(registerInfo: RegisterInfo?): Bundle {
|
||||
registerInfo?.let {
|
||||
putParcelable(KEY_REGISTER_INFO, it)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun removeInfoFromIntent(intent: Intent) {
|
||||
intent.removeExtra(KEY_SEARCH_INFO)
|
||||
intent.removeExtra(KEY_REGISTER_INFO)
|
||||
fun Intent.retrieveRegisterInfo(): RegisterInfo? {
|
||||
return getParcelableExtraCompat(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||
// TODO Replace by Intent.addSpecialMode
|
||||
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||
fun Bundle.getRegisterInfo(): RegisterInfo? {
|
||||
return getParcelableCompat(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun Intent.removeInfo() {
|
||||
removeExtra(KEY_SEARCH_INFO)
|
||||
removeExtra(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
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 Bundle.addSpecialMode(specialMode: SpecialMode): Bundle {
|
||||
this.putEnum(KEY_SPECIAL_MODE, specialMode)
|
||||
return this
|
||||
}
|
||||
|
||||
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||
// TODO Replace by Intent.addTypeMode
|
||||
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||
fun Intent.retrieveSpecialMode(): SpecialMode {
|
||||
return this.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||
}
|
||||
|
||||
fun Bundle.getSpecialMode(): SpecialMode {
|
||||
return this.getEnum<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve nodes ids from intent and get the corresponding entry info list in [database]
|
||||
*/
|
||||
fun Intent.retrieveAndRemoveEntries(database: ContextualDatabase): List<EntryInfo> {
|
||||
val nodesIds = retrieveNodesIds()
|
||||
?: throw IOException("NodesIds is null")
|
||||
removeNodesIds()
|
||||
return nodesIds.mapNotNull { nodeId ->
|
||||
database
|
||||
.getEntryById(NodeIdUUID(nodeId))
|
||||
?.getEntryInfo(database)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,74 +288,76 @@ 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
|
||||
&& (typeMode == TypeMode.MAGIKEYBOARD || typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||
|| (specialMode == SpecialMode.REGISTRATION
|
||||
&& typeMode == TypeMode.PASSKEY)
|
||||
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||
}
|
||||
|
||||
fun doSpecialAction(intent: Intent,
|
||||
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)) {
|
||||
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) {
|
||||
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
|
||||
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
||||
when (retrieveTypeModeFromIntent(intent)) {
|
||||
when (val typeMode = intent.retrieveTypeMode()) {
|
||||
TypeMode.DEFAULT -> {
|
||||
removeModesFromIntent(intent)
|
||||
intent.removeModes()
|
||||
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)
|
||||
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 {
|
||||
@@ -316,26 +367,22 @@ object EntrySelectionHelper {
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
||||
if (!isIntentSenderMode(
|
||||
specialMode = retrieveSpecialModeFromIntent(intent),
|
||||
typeMode = retrieveTypeModeFromIntent(intent))
|
||||
) {
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
}
|
||||
when (retrieveTypeModeFromIntent(intent)) {
|
||||
TypeMode.AUTOFILL -> {
|
||||
autofillRegistrationAction.invoke(registerInfo)
|
||||
}
|
||||
TypeMode.PASSKEY -> {
|
||||
passkeyRegistrationAction.invoke(registerInfo)
|
||||
}
|
||||
else -> {
|
||||
// Do other registration type
|
||||
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
|
||||
val typeMode = intent.retrieveTypeMode()
|
||||
val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
|
||||
if (!intentSenderMode) {
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
}
|
||||
if (registerInfo != null)
|
||||
registrationAction.invoke(
|
||||
intentSenderMode,
|
||||
typeMode,
|
||||
registerInfo
|
||||
)
|
||||
else {
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,7 +414,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)
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.credentialprovider
|
||||
enum class SpecialMode {
|
||||
DEFAULT,
|
||||
SEARCH,
|
||||
SAVE,
|
||||
SELECTION,
|
||||
REGISTRATION;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.kunzisoft.keepass.credentialprovider
|
||||
|
||||
enum class TypeMode {
|
||||
DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY
|
||||
DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.kunzisoft.keepass.credentialprovider
|
||||
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.view.ProtectedFieldView
|
||||
|
||||
data class UserVerificationData(
|
||||
val actionType: UserVerificationActionType,
|
||||
val database: ContextualDatabase? = null,
|
||||
val entryId: NodeId<*>? = null,
|
||||
val field: Field? = null,
|
||||
val protectedFieldView: ProtectedFieldView? = null,
|
||||
val preferenceKey: String? = null
|
||||
)
|
||||
|
||||
enum class UserVerificationActionType {
|
||||
LAUNCH_PASSKEY_CEREMONY,
|
||||
SHOW_PROTECTED_FIELD,
|
||||
COPY_PROTECTED_FIELD,
|
||||
EDIT_ENTRY,
|
||||
EDIT_DATABASE_SETTING,
|
||||
MERGE_FROM_DATABASE,
|
||||
SAVE_DATABASE_COPY_TO
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package com.kunzisoft.keepass.credentialprovider
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.CheckDatabaseCredentialDialogFragment
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil.isUserVerificationDeviceCredential
|
||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||
import com.kunzisoft.keepass.view.ProtectedFieldView
|
||||
import com.kunzisoft.keepass.view.toastError
|
||||
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
|
||||
|
||||
class UserVerificationHelper {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_USER_VERIFICATION = "com.kunzisoft.keepass.extra.userVerification"
|
||||
private const val EXTRA_USER_VERIFIED_WITH_AUTH = "com.kunzisoft.keepass.extra.userVerifiedWithAuth"
|
||||
|
||||
/**
|
||||
* Allowed authenticators for the User Verification
|
||||
*/
|
||||
const val ALLOWED_AUTHENTICATORS = BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||
|
||||
/**
|
||||
* Check if the device supports the biometric prompt for User Verification
|
||||
*/
|
||||
fun Context.isAuthenticatorsAllowed(): Boolean {
|
||||
return BiometricManager.from(this)
|
||||
.canAuthenticate(ALLOWED_AUTHENTICATORS) == BIOMETRIC_SUCCESS
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the User Verification to the intent
|
||||
*/
|
||||
fun Intent.addUserVerification(
|
||||
userVerification: UserVerificationRequirement,
|
||||
userVerifiedWithAuth: Boolean
|
||||
) {
|
||||
putEnumExtra(EXTRA_USER_VERIFICATION, userVerification)
|
||||
putExtra(EXTRA_USER_VERIFIED_WITH_AUTH, userVerifiedWithAuth)
|
||||
}
|
||||
|
||||
/**
|
||||
* Define if the User is verified with authentification from the intent
|
||||
*/
|
||||
fun Intent.getUserVerifiedWithAuth(): Boolean {
|
||||
return getBooleanExtra(EXTRA_USER_VERIFIED_WITH_AUTH, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the User Verification from the intent
|
||||
*/
|
||||
fun Intent.removeUserVerification() {
|
||||
removeExtra(EXTRA_USER_VERIFICATION)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the User verified with auth from the intent
|
||||
*/
|
||||
fun Intent.removeUserVerifiedWithAuth() {
|
||||
removeExtra(EXTRA_USER_VERIFIED_WITH_AUTH)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User Verification from the intent
|
||||
*/
|
||||
fun Intent.isUserVerificationNeeded(userVerificationPreferred: Boolean): Boolean {
|
||||
val userVerification: UserVerificationRequirement =
|
||||
getEnumExtra<UserVerificationRequirement>(EXTRA_USER_VERIFICATION)
|
||||
?: UserVerificationRequirement.PREFERRED
|
||||
return (userVerification == UserVerificationRequirement.REQUIRED
|
||||
|| (userVerificationPreferred
|
||||
&& userVerification == UserVerificationRequirement.PREFERRED)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the User needs to be verified for this entry
|
||||
*/
|
||||
fun EntryInfo.isUserVerificationNeeded(): Boolean {
|
||||
// Apply to any entry with protected content
|
||||
// Not only this.passkey != null
|
||||
return true
|
||||
}
|
||||
|
||||
fun Fragment.checkUserVerification(
|
||||
userVerificationViewModel: UserVerificationViewModel,
|
||||
dataToVerify: UserVerificationData
|
||||
) {
|
||||
activity?.checkUserVerification(userVerificationViewModel, dataToVerify)
|
||||
}
|
||||
|
||||
fun FragmentActivity.requestShowUnprotectField(
|
||||
userVerificationViewModel: UserVerificationViewModel,
|
||||
database: ContextualDatabase,
|
||||
protectedFieldView: ProtectedFieldView
|
||||
) {
|
||||
if (protectedFieldView.isCurrentlyProtected()) {
|
||||
checkUserVerification(
|
||||
userVerificationViewModel = userVerificationViewModel,
|
||||
dataToVerify = UserVerificationData(
|
||||
actionType = UserVerificationActionType.SHOW_PROTECTED_FIELD,
|
||||
database = database,
|
||||
protectedFieldView = protectedFieldView
|
||||
)
|
||||
)
|
||||
} else {
|
||||
protectedFieldView.protect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog to verify the user
|
||||
*/
|
||||
fun FragmentActivity.checkUserVerification(
|
||||
userVerificationViewModel: UserVerificationViewModel,
|
||||
dataToVerify: UserVerificationData
|
||||
) {
|
||||
if (isAuthenticatorsAllowed() && isUserVerificationDeviceCredential(this)) {
|
||||
showUserVerificationDeviceCredential(userVerificationViewModel, dataToVerify)
|
||||
} else if (dataToVerify.database != null) {
|
||||
showUserVerificationDatabaseCredential(userVerificationViewModel, dataToVerify)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog for entering the device credential to be checked
|
||||
*/
|
||||
fun FragmentActivity.showUserVerificationDeviceCredential(
|
||||
userVerificationViewModel: UserVerificationViewModel,
|
||||
dataToVerify: UserVerificationData
|
||||
) {
|
||||
BiometricPrompt(
|
||||
this, ContextCompat.getMainExecutor(this),
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence
|
||||
) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
when (errorCode) {
|
||||
BiometricPrompt.ERROR_CANCELED,
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||
BiometricPrompt.ERROR_USER_CANCELED -> {
|
||||
// No operation
|
||||
Log.i("UserVerification", "$errString")
|
||||
}
|
||||
else -> {
|
||||
toastError(SecurityException("Authentication error: $errString"))
|
||||
}
|
||||
}
|
||||
userVerificationViewModel.onUserVerificationFailed(dataToVerify)
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
userVerificationViewModel.onUserVerificationSucceeded(dataToVerify)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
toastError(SecurityException(getString(R.string.device_unlock_not_recognized)))
|
||||
userVerificationViewModel.onUserVerificationFailed(dataToVerify)
|
||||
}
|
||||
}).authenticate(
|
||||
BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.user_verification_required_title))
|
||||
.setSubtitle(getString(R.string.user_verification_required_description))
|
||||
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog for entering the database credential to be checked
|
||||
*/
|
||||
fun FragmentActivity.showUserVerificationDatabaseCredential(
|
||||
userVerificationViewModel: UserVerificationViewModel,
|
||||
dataToVerify: UserVerificationData
|
||||
) {
|
||||
userVerificationViewModel.dataToVerify = dataToVerify
|
||||
val fragmentTag = "checkDatabaseCredentialDialog"
|
||||
var fragment: CheckDatabaseCredentialDialogFragment? =
|
||||
supportFragmentManager.findFragmentByTag(fragmentTag)
|
||||
as? CheckDatabaseCredentialDialogFragment?
|
||||
if (fragment == null) {
|
||||
fragment = CheckDatabaseCredentialDialogFragment.getInstance()
|
||||
fragment.show(this.supportFragmentManager, fragmentTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,245 +27,186 @@ 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.getRegisterInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSpecialMode
|
||||
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.autofill.AutofillHelper.retrieveAutofillComponent
|
||||
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()
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
autofillLauncherViewModel.manageSelectionResult(it)
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
autofillLauncherViewModel.manageRegistrationResult(it)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
override fun applyCustomStyle(): Boolean = false
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Not an autofill call
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun finishActivityIfReloadRequested(): Boolean = true
|
||||
|
||||
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
|
||||
)
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// To apply the bypass https://github.com/Kunzisoft/KeePassDX/issues/2238
|
||||
// before managing intent in super class
|
||||
intent.retrieveSelectionBundle()?.apply {
|
||||
intent.addSpecialMode(getSpecialMode())
|
||||
intent.addSearchInfo(getSearchInfo())
|
||||
intent.addRegisterInfo(getRegisterInfo())
|
||||
intent.addAutofillComponent(retrieveAutofillComponent())
|
||||
}
|
||||
)
|
||||
} else {
|
||||
super.onCreate(savedInstanceState)
|
||||
autofillLauncherViewModel.initialize()
|
||||
lifecycleScope.launch {
|
||||
// Initialize the parameters
|
||||
autofillLauncherViewModel.uiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
AutofillLauncherViewModel.UIState.Loading -> {}
|
||||
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
|
||||
showBlockRestartMessage()
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
autofillLauncherViewModel.cancelResult()
|
||||
}
|
||||
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
|
||||
showAutofillSuggestionMessage()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
// Retrieve the UI
|
||||
autofillLauncherViewModel.credentialUiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is CredentialLauncherViewModel.CredentialState.Loading -> {}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
|
||||
GroupActivity.launchForSelection(
|
||||
context = this@AutofillLauncherActivity,
|
||||
database = uiState.database,
|
||||
searchInfo = uiState.searchInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mAutofillSelectionActivityResultLauncher,
|
||||
)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
|
||||
GroupActivity.launchForRegistration(
|
||||
context = this,
|
||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
context = this@AutofillLauncherActivity,
|
||||
database = uiState.database,
|
||||
registerInfo = uiState.registerInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mAutofillRegistrationActivityResultLauncher
|
||||
)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||
FileDatabaseSelectActivity.launchForSelection(
|
||||
context = this@AutofillLauncherActivity,
|
||||
searchInfo = uiState.searchInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mAutofillSelectionActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||
FileDatabaseSelectActivity.launchForRegistration(
|
||||
context = this,
|
||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
context = this@AutofillLauncherActivity,
|
||||
registerInfo = uiState.registerInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||
setActivityResult(
|
||||
lockDatabase = uiState.lockDatabase,
|
||||
resultCode = uiState.resultCode,
|
||||
data = uiState.data
|
||||
)
|
||||
} else {
|
||||
showBlockRestartMessage()
|
||||
setResult(RESULT_CANCELED)
|
||||
}
|
||||
finish()
|
||||
is CredentialLauncherViewModel.CredentialState.ShowError -> {
|
||||
toastError(uiState.error)
|
||||
autofillLauncherViewModel.cancelResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 showReadOnlySaveMessage() {
|
||||
toastError(RegisterInReadOnlyDatabaseException())
|
||||
private fun showAutofillSuggestionMessage() {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.autofill_inline_suggestions_keyboard,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_PENDING_INTENT_BUNDLE = "com.kunzisoft.keepass.extra.BUNDLE"
|
||||
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? {
|
||||
try {
|
||||
return PendingIntent.getActivity(
|
||||
context, 0,
|
||||
// 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)
|
||||
fun Intent.retrieveSelectionBundle(): Bundle? {
|
||||
return this.getBundleExtra(KEY_PENDING_INTENT_BUNDLE)
|
||||
}
|
||||
})
|
||||
|
||||
fun getPendingIntentForSelection(
|
||||
context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
autofillComponent: AutofillComponent
|
||||
): PendingIntent? {
|
||||
try {
|
||||
// Doesn't work with direct extra Parcelable in Android 11 (don't know why?)
|
||||
// https://github.com/Kunzisoft/KeePassDX/issues/2238
|
||||
// Wrap into a bundle to bypass the problem
|
||||
val tempBundle = Bundle().apply {
|
||||
addSpecialMode(SpecialMode.SELECTION)
|
||||
addSearchInfo(searchInfo)
|
||||
addAutofillComponent(autofillComponent)
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
randomRequestCode(),
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
@@ -279,14 +220,21 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun getPendingIntentForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): PendingIntent? {
|
||||
fun getPendingIntentForRegistration(
|
||||
context: Context,
|
||||
registerInfo: RegisterInfo
|
||||
): PendingIntent? {
|
||||
try {
|
||||
// Bypass intent issue
|
||||
val tempBundle = Bundle().apply {
|
||||
addSpecialMode(SpecialMode.REGISTRATION)
|
||||
addRegisterInfo(registerInfo)
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context, 0,
|
||||
context,
|
||||
randomRequestCode(),
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
@@ -299,14 +247,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,22 +22,22 @@ 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 androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.addSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.EntrySelectionViewModel
|
||||
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
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Activity to search or select entry in database,
|
||||
@@ -45,198 +45,133 @@ import com.kunzisoft.keepass.view.toastError
|
||||
*/
|
||||
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
private val entrySelectionViewModel: EntrySelectionViewModel by viewModels()
|
||||
|
||||
private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
entrySelectionViewModel.manageSelectionResult(it)
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return false
|
||||
}
|
||||
override fun applyCustomStyle() = false
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
override fun finishActivityIfReloadRequested() = false
|
||||
|
||||
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
||||
if (keySelectionBundle != null) {
|
||||
// To manage package name
|
||||
var searchInfo = SearchInfo()
|
||||
keySelectionBundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
|
||||
searchInfo = mSearchInfo
|
||||
}
|
||||
launch(database, searchInfo)
|
||||
} else {
|
||||
// To manage share
|
||||
var sharedWebDomain: String? = null
|
||||
var otpString: String? = null
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
if ("text/plain" == intent.type) {
|
||||
// Retrieve web domain or OTP
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
else
|
||||
sharedWebDomain = extra.toUri().host
|
||||
}
|
||||
}
|
||||
launchSelection(database, sharedWebDomain, otpString)
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
// Retrieve OTP
|
||||
intent.dataString?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
}
|
||||
launchSelection(database, sharedWebDomain, otpString)
|
||||
}
|
||||
else -> {
|
||||
if (database != null) {
|
||||
GroupActivity.launch(this, database)
|
||||
} else {
|
||||
FileDatabaseSelectActivity.launch(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
entrySelectionViewModel.initialize()
|
||||
lifecycleScope.launch {
|
||||
// Initialize the parameters
|
||||
entrySelectionViewModel.uiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is EntrySelectionViewModel.UIState.Loading -> {}
|
||||
is EntrySelectionViewModel.UIState.PopulateKeyboard -> {
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
entry = uiState.entryInfo,
|
||||
toast = true
|
||||
)
|
||||
}
|
||||
is EntrySelectionViewModel.UIState.LaunchFileDatabaseSelectForSearch -> {
|
||||
FileDatabaseSelectActivity.launchForSearch(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
searchInfo = uiState.searchInfo
|
||||
)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun launchSelection(database: ContextualDatabase?,
|
||||
sharedWebDomain: String?,
|
||||
otpString: String?) {
|
||||
// Build domain search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
this.webDomain = sharedWebDomain
|
||||
this.otpString = otpString
|
||||
is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> {
|
||||
GroupActivity.launchForSearch(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
database = uiState.database,
|
||||
searchInfo = uiState.searchInfo
|
||||
)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
// Retrieve the UI
|
||||
entrySelectionViewModel.credentialUiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is CredentialLauncherViewModel.CredentialState.Loading -> {}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
|
||||
GroupActivity.launchForSelection(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
database = uiState.database,
|
||||
searchInfo = uiState.searchInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mEntrySelectionActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
|
||||
GroupActivity.launchForRegistration(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
database = uiState.database,
|
||||
registerInfo = uiState.registerInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = null // Null to not get any callback
|
||||
)
|
||||
finish()
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||
FileDatabaseSelectActivity.launchForSelection(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
searchInfo = uiState.searchInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mEntrySelectionActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||
FileDatabaseSelectActivity.launchForRegistration(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
registerInfo = uiState.registerInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = null // Null to not get any callback
|
||||
)
|
||||
finish()
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||
setActivityResult(
|
||||
lockDatabase = uiState.lockDatabase,
|
||||
resultCode = uiState.resultCode,
|
||||
data = uiState.data
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.ShowError -> {
|
||||
toastError(uiState.error)
|
||||
entrySelectionViewModel.cancelResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launch(database, searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launch(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo) {
|
||||
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onUnknownDatabaseRetrieved(database)
|
||||
entrySelectionViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||
}
|
||||
|
||||
// Setting to integrate Magikeyboard
|
||||
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
|
||||
|
||||
// If database is open
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
// Items found
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
toastError(RegisterInReadOnlyDatabaseException())
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
// Automatically populate keyboard
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entryInfo
|
||||
)
|
||||
},
|
||||
{ autoSearch ->
|
||||
GroupActivity.launchForKeyboardSelectionResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
autoSearch
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true
|
||||
)
|
||||
}
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
toastError(RegisterInReadOnlyDatabaseException())
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
GroupActivity.launchForKeyboardSelectionResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false
|
||||
)
|
||||
}
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
if (searchInfo.otpString != null) {
|
||||
FileDatabaseSelectActivity.launchForSaveResult(
|
||||
this,
|
||||
searchInfo
|
||||
)
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(
|
||||
this,
|
||||
searchInfo
|
||||
)
|
||||
} else {
|
||||
FileDatabaseSelectActivity.launchForSearchResult(
|
||||
this,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||
|
||||
fun launch(context: Context,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
|
||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||
})
|
||||
}
|
||||
fun launch(
|
||||
context: Context,
|
||||
searchInfo: SearchInfo? = null
|
||||
) {
|
||||
context.startActivity(Intent(
|
||||
context,
|
||||
EntrySelectionLauncherActivity::class.java
|
||||
).apply {
|
||||
addSearchInfo(searchInfo)
|
||||
// New task needed because don't launch from an Activity context
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.CredentialState.SetActivityResult -> {
|
||||
setActivityResult(
|
||||
lockDatabase = uiState.lockDatabase,
|
||||
resultCode = uiState.resultCode,
|
||||
data = uiState.data
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,28 +31,41 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
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
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.addUserVerification
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerifiedWithAuth
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
|
||||
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.settings.PreferencesUtil.isUserVerificationPreferred
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import com.kunzisoft.keepass.view.toastError
|
||||
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
@@ -60,6 +73,7 @@ import java.util.UUID
|
||||
class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||
|
||||
private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels()
|
||||
private val userVerificationViewModel: UserVerificationViewModel by viewModels()
|
||||
|
||||
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
@@ -79,15 +93,12 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
lifecycleScope.launch {
|
||||
// Initialize the parameters
|
||||
passkeyLauncherViewModel.initialize()
|
||||
passkeyLauncherViewModel.initialize(userVerified = intent.getUserVerifiedWithAuth())
|
||||
// Retrieve the UI
|
||||
passkeyLauncherViewModel.uiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
@@ -105,61 +116,116 @@ 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.CredentialState.Loading -> {}
|
||||
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||
setActivityResult(
|
||||
lockDatabase = uiState.lockDatabase,
|
||||
resultCode = uiState.resultCode,
|
||||
data = uiState.data
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.ShowError -> {
|
||||
toastError(uiState.error)
|
||||
passkeyLauncherViewModel.cancelResult()
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
|
||||
GroupActivity.launchForSelection(
|
||||
context = this@PasskeyLauncherActivity,
|
||||
database = uiState.database,
|
||||
typeMode = uiState.typeMode,
|
||||
searchInfo = uiState.searchInfo,
|
||||
activityResultLauncher = mPasskeySelectionActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
|
||||
GroupActivity.launchForRegistration(
|
||||
context = this@PasskeyLauncherActivity,
|
||||
database = uiState.database,
|
||||
typeMode = uiState.typeMode,
|
||||
registerInfo = uiState.registerInfo,
|
||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||
FileDatabaseSelectActivity.launchForSelection(
|
||||
context = this@PasskeyLauncherActivity,
|
||||
typeMode = uiState.typeMode,
|
||||
searchInfo = uiState.searchInfo,
|
||||
activityResultLauncher = mPasskeySelectionActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||
FileDatabaseSelectActivity.launchForRegistration(
|
||||
context = this@PasskeyLauncherActivity,
|
||||
typeMode = uiState.typeMode,
|
||||
registerInfo = uiState.registerInfo,
|
||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
userVerificationViewModel.userVerificationState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is UserVerificationViewModel.UVState.Loading -> {}
|
||||
is UserVerificationViewModel.UVState.OnUserVerificationSucceeded -> {
|
||||
val data = uiState.dataToVerify
|
||||
when (data.actionType) {
|
||||
UserVerificationActionType.LAUNCH_PASSKEY_CEREMONY -> {
|
||||
passkeyLauncherViewModel.launchActionIfNeeded(
|
||||
userVerified = true,
|
||||
intent = intent,
|
||||
specialMode = mSpecialMode,
|
||||
database = uiState.dataToVerify.database
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
userVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
is UserVerificationViewModel.UVState.OnUserVerificationCanceled -> {
|
||||
toastError(uiState.error)
|
||||
passkeyLauncherViewModel.cancelResult()
|
||||
userVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database)
|
||||
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onUnknownDatabaseRetrieved(database)
|
||||
// To manage https://github.com/Kunzisoft/KeePassDX/issues/2283
|
||||
val userVerificationNeeded = intent.isUserVerificationNeeded(
|
||||
userVerificationPreferred = isUserVerificationPreferred(this)
|
||||
) && intent.getUserVerifiedWithAuth().not()
|
||||
if (userVerificationNeeded) {
|
||||
checkUserVerification(
|
||||
userVerificationViewModel = userVerificationViewModel,
|
||||
dataToVerify = UserVerificationData(
|
||||
actionType = UserVerificationActionType.LAUNCH_PASSKEY_CEREMONY,
|
||||
database = database
|
||||
)
|
||||
)
|
||||
} else {
|
||||
passkeyLauncherViewModel.launchActionIfNeeded(
|
||||
intent = intent,
|
||||
specialMode = mSpecialMode,
|
||||
database = database
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -170,7 +236,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 +301,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) { _, _ ->
|
||||
@@ -269,11 +338,13 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||
specialMode: SpecialMode,
|
||||
searchInfo: SearchInfo? = null,
|
||||
appOrigin: AppOrigin? = null,
|
||||
nodeId: UUID? = null
|
||||
nodeId: UUID? = null,
|
||||
userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED,
|
||||
userVerifiedWithAuth: Boolean = true
|
||||
): PendingIntent? {
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
(Math.random() * Integer.MAX_VALUE).toInt(),
|
||||
randomRequestCode(),
|
||||
Intent(context, PasskeyLauncherActivity::class.java).apply {
|
||||
addSpecialMode(specialMode)
|
||||
addTypeMode(TypeMode.PASSKEY)
|
||||
@@ -281,6 +352,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||
addAppOrigin(appOrigin)
|
||||
addNodeId(nodeId)
|
||||
addAuthCode(nodeId)
|
||||
addUserVerification(userVerification, userVerifiedWithAuth)
|
||||
},
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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
|
||||
@@ -28,6 +27,7 @@ import android.content.Intent
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.Field
|
||||
import android.service.autofill.FillResponse
|
||||
@@ -38,7 +38,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 +53,63 @@ 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.getParcelableCompat
|
||||
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?): Intent {
|
||||
autofillComponent?.let {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Intent.retrieveAutofillComponent(): AutofillComponent? {
|
||||
this.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))
|
||||
AutofillComponent(
|
||||
assistStructure,
|
||||
this.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||
} else {
|
||||
AutofillComponent(assistStructure, null)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun Bundle.addAutofillComponent(autofillComponent: AutofillComponent?): Bundle {
|
||||
autofillComponent?.let {
|
||||
this.putParcelable(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||
this.putParcelable(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Bundle.retrieveAutofillComponent(): AutofillComponent? {
|
||||
this.getParcelableCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
AutofillComponent(
|
||||
assistStructure,
|
||||
this.getParcelableCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
)
|
||||
} else {
|
||||
AutofillComponent(assistStructure, null)
|
||||
}
|
||||
@@ -127,11 +168,13 @@ object AutofillHelper {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun buildDatasetForEntry(context: Context,
|
||||
private fun buildDatasetForEntry(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?): Dataset {
|
||||
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 +334,13 @@ object AutofillHelper {
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun buildInlinePresentationForEntry(context: Context,
|
||||
private fun buildInlinePresentationForEntry(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo): InlinePresentation? {
|
||||
entryInfo: EntryInfo
|
||||
): InlinePresentation? {
|
||||
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||
@@ -314,7 +359,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 +386,11 @@ object AutofillHelper {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForManualSelection(context: Context,
|
||||
private fun buildInlinePresentationForManualSelection(
|
||||
context: Context,
|
||||
inlinePresentationSpec: InlinePresentationSpec,
|
||||
pendingIntent: PendingIntent): InlinePresentation? {
|
||||
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 +407,13 @@ object AutofillHelper {
|
||||
}.build().slice, inlinePresentationSpec, false)
|
||||
}
|
||||
|
||||
fun buildResponse(context: Context,
|
||||
fun buildResponse(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
||||
autofillComponent: AutofillComponent
|
||||
): FillResponse? {
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
// Add Header
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -385,7 +434,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 +451,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,13 +483,20 @@ 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 ->
|
||||
autofillComponent.compatInlineSuggestionsRequest
|
||||
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpec =
|
||||
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
inlinePresentation = buildInlinePresentationForManualSelection(
|
||||
@@ -486,61 +549,31 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Autofill response for one entry
|
||||
* Build the Autofill response
|
||||
*/
|
||||
fun buildResponseAndSetResult(activity: Activity,
|
||||
fun buildResponse(
|
||||
context: Context,
|
||||
autofillComponent: AutofillComponent,
|
||||
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>) {
|
||||
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 ->
|
||||
StructureParser(autofillComponent.assistStructure).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(
|
||||
onIntentCreated(Intent().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)
|
||||
}
|
||||
buildResponse(
|
||||
context = context,
|
||||
database = database,
|
||||
entriesInfo = entriesInfo,
|
||||
parseResult = result,
|
||||
autofillComponent = autofillComponent
|
||||
)
|
||||
))
|
||||
} ?: throw IOException("Unable to parse the structure")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
override fun onFillRequest(
|
||||
request: FillRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: FillCallback) {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSelection(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
val autofillComponent = AutofillComponent(
|
||||
latestStructure,
|
||||
inlineSuggestionsRequest
|
||||
)
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = database,
|
||||
database = mDatabase,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
callback.onSuccess(
|
||||
AutofillHelper.buildResponse(
|
||||
this, openedDatabase,
|
||||
items, parseResult, inlineSuggestionsRequest
|
||||
context = this,
|
||||
database = openedDatabase,
|
||||
entriesInfo = items,
|
||||
parseResult = parseResult,
|
||||
autofillComponent = autofillComponent
|
||||
)
|
||||
)
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult, openedDatabase,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
searchInfo, autofillComponent, callback)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult, null,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
searchInfo, autofillComponent, callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
private fun showUIForEntrySelection(
|
||||
parseResult: StructureParser.Result,
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
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 registerInfo = RegisterInfo(
|
||||
searchInfo = SearchInfo().apply {
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
},
|
||||
}
|
||||
val registerInfo = RegisterInfo(
|
||||
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)
|
||||
AutofillLauncherActivity.getPendingIntentForRegistration(
|
||||
this,
|
||||
registerInfo
|
||||
)?.intentSender?.let { intentSender ->
|
||||
success = true
|
||||
callback.onSuccess()
|
||||
//}
|
||||
callback.onSuccess(intentSender)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
||||
// Password Id changed if it's the second times we are here,
|
||||
// So the last username candidate is most appropriate
|
||||
if (result?.passwordId != null) {
|
||||
if (result?.passwordId != null && usernameIdCandidate != null) {
|
||||
result?.usernameId = usernameIdCandidate
|
||||
result?.usernameValue = usernameValueCandidate
|
||||
}
|
||||
@@ -362,9 +362,9 @@ 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)}")
|
||||
}
|
||||
}
|
||||
inputIsVariationType(inputType,
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||
// Some forms used visible password as username
|
||||
|
||||
@@ -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
|
||||
@@ -461,9 +462,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
||||
}
|
||||
|
||||
fun performSelection(items: List<EntryInfo>,
|
||||
fun performSelection(
|
||||
items: List<EntryInfo>,
|
||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||
actionEntrySelection: (autoSearch: Boolean) -> Unit
|
||||
) {
|
||||
EntrySelectionHelper.performSelection(
|
||||
items = items,
|
||||
actionPopulateCredentialProvider = { itemFound ->
|
||||
@@ -477,15 +480,5 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
actionEntrySelection = actionEntrySelection
|
||||
)
|
||||
}
|
||||
|
||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||
entry: EntryInfo,
|
||||
toast: Boolean = true) {
|
||||
// Populate Magikeyboard with entry
|
||||
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||
// Consume the selection mode
|
||||
EntrySelectionHelper.removeModesFromIntent(activity.intent)
|
||||
activity.moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,12 +42,14 @@ import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.CredentialProviderService
|
||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||
@@ -90,56 +92,67 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
|
||||
private fun buildPasskeySearchInfo(
|
||||
relyingParty: String,
|
||||
credentialIds: List<String> = listOf()
|
||||
): SearchInfo {
|
||||
return SearchInfo().apply {
|
||||
this.relyingParty = relyingParty
|
||||
this.isAPasskeySearch = true
|
||||
this.query = relyingParty
|
||||
this.credentialIds = credentialIds
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
toastError(e)
|
||||
callback.onError(GetCredentialUnknownException())
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId
|
||||
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
||||
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||
val publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions(option.requestJson)
|
||||
val relyingPartyId = publicKeyCredentialRequestOptions.rpId
|
||||
val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials
|
||||
.map { b64Encode(it.id) }
|
||||
val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList)
|
||||
val userVerification = publicKeyCredentialRequestOptions.userVerification
|
||||
Log.d(TAG, "Build passkey search for UV $userVerification, " +
|
||||
"RP $relyingPartyId and Credential IDs $credentialIdList")
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = mDatabase,
|
||||
@@ -151,14 +164,19 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
context = applicationContext,
|
||||
specialMode = SpecialMode.SELECTION,
|
||||
nodeId = passkeyEntry.id,
|
||||
appOrigin = passkeyEntry.appOrigin
|
||||
appOrigin = passkeyEntry.appOrigin,
|
||||
userVerification = userVerification,
|
||||
userVerifiedWithAuth = false
|
||||
)?.let { usagePendingIntent ->
|
||||
val passkey = passkeyEntry.passkey
|
||||
passkeyEntries.add(
|
||||
PublicKeyCredentialEntry(
|
||||
context = applicationContext,
|
||||
username = passkey?.username ?: "Unknown",
|
||||
icon = passkeyEntry.buildIcon(this@PasskeyProviderService, database)?.apply {
|
||||
icon = passkeyEntry.buildIcon(
|
||||
this@PasskeyProviderService,
|
||||
database
|
||||
)?.apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
} ?: defaultIcon,
|
||||
pendingIntent = usagePendingIntent,
|
||||
@@ -169,14 +187,18 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
)
|
||||
}
|
||||
}
|
||||
callback(passkeyEntries)
|
||||
},
|
||||
onItemNotFound = { _ ->
|
||||
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
||||
if (credentialIdList.isEmpty()) {
|
||||
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
||||
PasskeyLauncherActivity.getPendingIntent(
|
||||
context = applicationContext,
|
||||
specialMode = SpecialMode.SELECTION,
|
||||
searchInfo = searchInfo
|
||||
searchInfo = searchInfo,
|
||||
userVerification = userVerification,
|
||||
userVerifiedWithAuth = false
|
||||
)?.let { pendingIntent ->
|
||||
passkeyEntries.add(
|
||||
PublicKeyCredentialEntry(
|
||||
@@ -191,6 +213,16 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
)
|
||||
)
|
||||
}
|
||||
callback(passkeyEntries)
|
||||
} else {
|
||||
throw IOException(
|
||||
getString(
|
||||
R.string.error_passkey_credential_id,
|
||||
relyingPartyId,
|
||||
credentialIdList
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
||||
@@ -198,7 +230,8 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
PasskeyLauncherActivity.getPendingIntent(
|
||||
context = applicationContext,
|
||||
specialMode = SpecialMode.SELECTION,
|
||||
searchInfo = searchInfo
|
||||
searchInfo = searchInfo,
|
||||
userVerifiedWithAuth = true
|
||||
)?.let { pendingIntent ->
|
||||
passkeyEntries.add(
|
||||
PublicKeyCredentialEntry(
|
||||
@@ -213,9 +246,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
)
|
||||
)
|
||||
}
|
||||
callback(passkeyEntries)
|
||||
}
|
||||
)
|
||||
return passkeyEntries
|
||||
}
|
||||
|
||||
override fun onBeginCreateCredentialRequest(
|
||||
@@ -225,7 +258,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,27 +268,35 @@ 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
||||
accountName: String,
|
||||
searchInfo: SearchInfo?
|
||||
searchInfo: SearchInfo?,
|
||||
userVerification: UserVerificationRequirement
|
||||
) {
|
||||
Log.d(TAG, "Add pending intent for registration in opened database to create new item")
|
||||
// TODO add a setting to directly store in a specific group
|
||||
PasskeyLauncherActivity.getPendingIntent(
|
||||
context = applicationContext,
|
||||
specialMode = SpecialMode.REGISTRATION,
|
||||
searchInfo = searchInfo
|
||||
searchInfo = searchInfo,
|
||||
userVerification = userVerification,
|
||||
userVerifiedWithAuth = false
|
||||
)?.let { pendingIntent ->
|
||||
this.add(
|
||||
CreateEntry(
|
||||
@@ -266,15 +309,23 @@ 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(
|
||||
val publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions(
|
||||
requestJson = request.requestJson,
|
||||
clientDataHash = request.clientDataHash
|
||||
).relyingPartyEntity.id
|
||||
)
|
||||
val relyingPartyId = publicKeyCredentialCreationOptions.relyingPartyEntity.id
|
||||
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
||||
val userVerification = publicKeyCredentialCreationOptions.authenticatorSelection.userVerification
|
||||
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
@@ -285,7 +336,11 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
throw RegisterInReadOnlyDatabaseException()
|
||||
} else {
|
||||
// To create a new entry
|
||||
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||
createEntries.addPendingIntentCreationNewEntry(
|
||||
accountName = accountName,
|
||||
searchInfo = searchInfo,
|
||||
userVerification = userVerification
|
||||
)
|
||||
/* TODO Overwrite
|
||||
// To select an existing entry and permit an overwrite
|
||||
Log.w(TAG, "Passkey already registered")
|
||||
@@ -309,21 +364,28 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
}
|
||||
}*/
|
||||
}
|
||||
callback(createEntries)
|
||||
},
|
||||
onItemNotFound = { database ->
|
||||
// To create a new entry
|
||||
if (database.isReadOnly) {
|
||||
throw RegisterInReadOnlyDatabaseException()
|
||||
} else {
|
||||
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||
createEntries.addPendingIntentCreationNewEntry(
|
||||
accountName = accountName,
|
||||
searchInfo = searchInfo,
|
||||
userVerification = userVerification
|
||||
)
|
||||
}
|
||||
callback(createEntries)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// Launch the passkey launcher activity to open the database
|
||||
Log.d(TAG, "Add pending intent for passkey registration in closed database")
|
||||
PasskeyLauncherActivity.getPendingIntent(
|
||||
context = applicationContext,
|
||||
specialMode = SpecialMode.REGISTRATION
|
||||
specialMode = SpecialMode.REGISTRATION,
|
||||
userVerifiedWithAuth = true
|
||||
)?.let { pendingIntent ->
|
||||
createEntries.add(
|
||||
CreateEntry(
|
||||
@@ -334,10 +396,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
)
|
||||
)
|
||||
}
|
||||
callback(createEntries)
|
||||
}
|
||||
)
|
||||
|
||||
return BeginCreateCredentialResponse(createEntries)
|
||||
}
|
||||
|
||||
override fun onClearCredentialStateRequest(
|
||||
|
||||
@@ -16,7 +16,25 @@
|
||||
|
||||
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||
|
||||
data class PublicKeyCredentialRpEntity(val name: String, val id: String)
|
||||
import com.kunzisoft.encrypt.Base64Helper
|
||||
import org.json.JSONObject
|
||||
|
||||
data class PublicKeyCredentialRpEntity(
|
||||
val name: String,
|
||||
val id: String
|
||||
) {
|
||||
companion object {
|
||||
fun JSONObject.getPublicKeyCredentialRpEntity(
|
||||
parameterName: String
|
||||
): PublicKeyCredentialRpEntity {
|
||||
val rpJson = this.getJSONObject(parameterName)
|
||||
return PublicKeyCredentialRpEntity(
|
||||
rpJson.getString("name"),
|
||||
rpJson.getString("id")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class PublicKeyCredentialUserEntity(
|
||||
val name: String,
|
||||
@@ -42,9 +60,41 @@ data class PublicKeyCredentialUserEntity(
|
||||
result = 31 * result + displayName.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun JSONObject.getPublicKeyCredentialUserEntity(
|
||||
parameterName: String
|
||||
): PublicKeyCredentialUserEntity {
|
||||
val rpUser = this.getJSONObject(parameterName)
|
||||
return PublicKeyCredentialUserEntity(
|
||||
rpUser.getString("name"),
|
||||
Base64Helper.b64Decode(rpUser.getString("id")),
|
||||
rpUser.getString("displayName")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class PublicKeyCredentialParameters(val type: String, val alg: Long)
|
||||
data class PublicKeyCredentialParameters(
|
||||
val type: String,
|
||||
val alg: Long
|
||||
) {
|
||||
companion object {
|
||||
fun JSONObject.getPublicKeyCredentialParametersList(
|
||||
parameterName: String
|
||||
): List<PublicKeyCredentialParameters> {
|
||||
val pubKeyCredParamsJson = this.getJSONArray(parameterName)
|
||||
val pubKeyCredParamsTmp: MutableList<PublicKeyCredentialParameters> = mutableListOf()
|
||||
for (i in 0 until pubKeyCredParamsJson.length()) {
|
||||
val e = pubKeyCredParamsJson.getJSONObject(i)
|
||||
pubKeyCredParamsTmp.add(
|
||||
PublicKeyCredentialParameters(e.getString("type"), e.getLong("alg"))
|
||||
)
|
||||
}
|
||||
return pubKeyCredParamsTmp.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class PublicKeyCredentialDescriptor(
|
||||
val type: String,
|
||||
@@ -70,11 +120,104 @@ data class PublicKeyCredentialDescriptor(
|
||||
result = 31 * result + transports.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun JSONObject.getPublicKeyCredentialDescriptorList(
|
||||
parameterName: String
|
||||
): List<PublicKeyCredentialDescriptor> {
|
||||
val credentialsJson = this.getJSONArray(parameterName)
|
||||
val credentialsTmp: MutableList<PublicKeyCredentialDescriptor> = mutableListOf()
|
||||
for (i in 0 until credentialsJson.length()) {
|
||||
val credentialJson = credentialsJson.getJSONObject(i)
|
||||
|
||||
val transports: MutableList<String> = mutableListOf()
|
||||
val transportsJson = credentialJson.getJSONArray("transports")
|
||||
for (j in 0 until transportsJson.length()) {
|
||||
transports.add(transportsJson.getString(j))
|
||||
}
|
||||
credentialsTmp.add(
|
||||
PublicKeyCredentialDescriptor(
|
||||
type = credentialJson.getString("type"),
|
||||
id = Base64Helper.b64Decode(credentialJson.getString("id")),
|
||||
transports = transports
|
||||
)
|
||||
)
|
||||
}
|
||||
return credentialsTmp.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria
|
||||
data class AuthenticatorSelectionCriteria(
|
||||
val authenticatorAttachment: String,
|
||||
val residentKey: String,
|
||||
val requireResidentKey: Boolean = false,
|
||||
val userVerification: String = "preferred"
|
||||
)
|
||||
val authenticatorAttachment: String? = null,
|
||||
val residentKey: ResidentKeyRequirement? = null,
|
||||
val requireResidentKey: Boolean?,
|
||||
val userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED
|
||||
) {
|
||||
companion object {
|
||||
fun JSONObject.getAuthenticatorSelectionCriteria(
|
||||
parameterName: String
|
||||
): AuthenticatorSelectionCriteria {
|
||||
val authenticatorSelection = this.optJSONObject(parameterName)
|
||||
?: return AuthenticatorSelectionCriteria(requireResidentKey = null)
|
||||
val authenticatorAttachment = if (!authenticatorSelection.isNull("authenticatorAttachment"))
|
||||
authenticatorSelection.getString("authenticatorAttachment") else null
|
||||
var residentKey = if (!authenticatorSelection.isNull("residentKey"))
|
||||
ResidentKeyRequirement.fromString(authenticatorSelection.getString("residentKey"))
|
||||
else null
|
||||
val requireResidentKey = authenticatorSelection.optBoolean("requireResidentKey", false)
|
||||
val userVerification = UserVerificationRequirement
|
||||
.fromString(authenticatorSelection.optString("userVerification", "preferred"))
|
||||
?: UserVerificationRequirement.PREFERRED
|
||||
// https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement
|
||||
if (residentKey == null) {
|
||||
residentKey = if (requireResidentKey) {
|
||||
ResidentKeyRequirement.REQUIRED
|
||||
} else {
|
||||
ResidentKeyRequirement.DISCOURAGED
|
||||
}
|
||||
}
|
||||
return AuthenticatorSelectionCriteria(
|
||||
authenticatorAttachment = authenticatorAttachment,
|
||||
residentKey = residentKey,
|
||||
requireResidentKey = requireResidentKey,
|
||||
userVerification = userVerification
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement
|
||||
enum class ResidentKeyRequirement(val value: String) {
|
||||
DISCOURAGED("discouraged"),
|
||||
PREFERRED("preferred"),
|
||||
REQUIRED("required");
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
companion object {
|
||||
fun fromString(value: String): ResidentKeyRequirement? {
|
||||
return ResidentKeyRequirement.entries.firstOrNull {
|
||||
it.value.equals(other = value, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement
|
||||
enum class UserVerificationRequirement(val value: String) {
|
||||
REQUIRED("required"),
|
||||
PREFERRED("preferred"),
|
||||
DISCOURAGED("discouraged");
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
companion object {
|
||||
fun fromString(value: String): UserVerificationRequirement? {
|
||||
return UserVerificationRequirement.entries.firstOrNull {
|
||||
it.value.equals(other = value, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,52 +20,42 @@
|
||||
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||
|
||||
import com.kunzisoft.encrypt.Base64Helper
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorSelectionCriteria.Companion.getAuthenticatorSelectionCriteria
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialDescriptor.Companion.getPublicKeyCredentialDescriptorList
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialParameters.Companion.getPublicKeyCredentialParametersList
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRpEntity.Companion.getPublicKeyCredentialRpEntity
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUserEntity.Companion.getPublicKeyCredentialUserEntity
|
||||
import org.json.JSONObject
|
||||
|
||||
class PublicKeyCredentialCreationOptions(
|
||||
requestJson: String,
|
||||
var clientDataHash: ByteArray?
|
||||
) {
|
||||
val json: JSONObject = JSONObject(requestJson)
|
||||
private val json: JSONObject = JSONObject(requestJson)
|
||||
|
||||
val relyingPartyEntity: PublicKeyCredentialRpEntity
|
||||
val userEntity: PublicKeyCredentialUserEntity
|
||||
val challenge: ByteArray
|
||||
val pubKeyCredParams: List<PublicKeyCredentialParameters>
|
||||
val relyingPartyEntity: PublicKeyCredentialRpEntity =
|
||||
json.getPublicKeyCredentialRpEntity("rp")
|
||||
|
||||
var timeout: Long
|
||||
var excludeCredentials: List<PublicKeyCredentialDescriptor>
|
||||
var authenticatorSelection: AuthenticatorSelectionCriteria
|
||||
var attestation: String
|
||||
val userEntity: PublicKeyCredentialUserEntity =
|
||||
json.getPublicKeyCredentialUserEntity("user")
|
||||
|
||||
init {
|
||||
val rpJson = json.getJSONObject("rp")
|
||||
relyingPartyEntity = PublicKeyCredentialRpEntity(rpJson.getString("name"), rpJson.getString("id"))
|
||||
val rpUser = json.getJSONObject("user")
|
||||
val userId = Base64Helper.b64Decode(rpUser.getString("id"))
|
||||
userEntity =
|
||||
PublicKeyCredentialUserEntity(
|
||||
rpUser.getString("name"),
|
||||
userId,
|
||||
rpUser.getString("displayName")
|
||||
)
|
||||
challenge = Base64Helper.b64Decode(json.getString("challenge"))
|
||||
val pubKeyCredParamsJson = json.getJSONArray("pubKeyCredParams")
|
||||
val pubKeyCredParamsTmp: MutableList<PublicKeyCredentialParameters> = mutableListOf()
|
||||
for (i in 0 until pubKeyCredParamsJson.length()) {
|
||||
val e = pubKeyCredParamsJson.getJSONObject(i)
|
||||
pubKeyCredParamsTmp.add(
|
||||
PublicKeyCredentialParameters(e.getString("type"), e.getLong("alg"))
|
||||
)
|
||||
}
|
||||
pubKeyCredParams = pubKeyCredParamsTmp.toList()
|
||||
val challenge: ByteArray =
|
||||
Base64Helper.b64Decode(json.getString("challenge"))
|
||||
|
||||
timeout = json.optLong("timeout", 0)
|
||||
// TODO: Fix excludeCredentials and authenticatorSelection
|
||||
excludeCredentials = emptyList()
|
||||
authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required")
|
||||
attestation = json.optString("attestation", "none")
|
||||
}
|
||||
val pubKeyCredParams: List<PublicKeyCredentialParameters> =
|
||||
json.getPublicKeyCredentialParametersList("pubKeyCredParams")
|
||||
|
||||
var timeout: Long =
|
||||
json.optLong("timeout", 0)
|
||||
|
||||
var excludeCredentials: List<PublicKeyCredentialDescriptor> =
|
||||
json.getPublicKeyCredentialDescriptorList("excludeCredentials")
|
||||
|
||||
var authenticatorSelection: AuthenticatorSelectionCriteria =
|
||||
json.getAuthenticatorSelectionCriteria("authenticatorSelection")
|
||||
|
||||
var attestation: String =
|
||||
json.optString("attestation", "none")
|
||||
|
||||
companion object {
|
||||
private val TAG = PublicKeyCredentialCreationOptions::class.simpleName
|
||||
|
||||
@@ -20,12 +20,33 @@
|
||||
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||
|
||||
import com.kunzisoft.encrypt.Base64Helper
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialDescriptor.Companion.getPublicKeyCredentialDescriptorList
|
||||
import org.json.JSONObject
|
||||
|
||||
// https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement
|
||||
class PublicKeyCredentialRequestOptions(requestJson: String) {
|
||||
val json: JSONObject = JSONObject(requestJson)
|
||||
val challenge: ByteArray = Base64Helper.b64Decode(json.getString("challenge"))
|
||||
val timeout: Long = json.optLong("timeout", 0)
|
||||
val rpId: String = json.optString("rpId", "")
|
||||
val userVerification: String = json.optString("userVerification", "preferred")
|
||||
private val json: JSONObject = JSONObject(requestJson)
|
||||
|
||||
val challenge: ByteArray =
|
||||
Base64Helper.b64Decode(json.getString("challenge"))
|
||||
|
||||
val timeout: Long =
|
||||
json.optLong("timeout", 0)
|
||||
|
||||
val rpId: String =
|
||||
json.optString("rpId", "")
|
||||
|
||||
val allowCredentials: List<PublicKeyCredentialDescriptor> =
|
||||
json.getPublicKeyCredentialDescriptorList("allowCredentials")
|
||||
|
||||
val userVerification: UserVerificationRequirement =
|
||||
UserVerificationRequirement.fromString(
|
||||
json.optString("userVerification", "preferred"))
|
||||
?: UserVerificationRequirement.PREFERRED
|
||||
|
||||
// TODO Hints
|
||||
val hints: List<String> = listOf()
|
||||
|
||||
// TODO Extensions
|
||||
// val extensions: AuthenticationExtensionsClientInputs
|
||||
}
|
||||
@@ -22,9 +22,9 @@ package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
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 +44,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 +61,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 +88,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 +107,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 +146,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,19 +170,57 @@ 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
|
||||
* Build the Passkey error response
|
||||
*/
|
||||
fun Intent.retrieveNodeId(): UUID? {
|
||||
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
|
||||
fun Activity.buildPasskeyErrorAndSetResult(
|
||||
resources: Resources,
|
||||
relyingPartyId: String?,
|
||||
credentialIds: List<String>
|
||||
) {
|
||||
val error = resources.getString(
|
||||
R.string.error_passkey_credential_id,
|
||||
relyingPartyId,
|
||||
credentialIds
|
||||
)
|
||||
Log.e(javaClass.name, error)
|
||||
Toast.makeText(
|
||||
this,
|
||||
error,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,11 +411,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 +447,9 @@ object PasskeyHelper {
|
||||
privateKeyPem = privateKeyPem,
|
||||
credentialId = b64Encode(credentialId),
|
||||
userHandle = b64Encode(userHandle),
|
||||
relyingParty = relyingParty
|
||||
relyingParty = relyingParty,
|
||||
backupEligibility = defaultBackupEligibility,
|
||||
backupState = defaultBackupState
|
||||
)
|
||||
|
||||
// create new entry in database
|
||||
@@ -501,6 +494,7 @@ object PasskeyHelper {
|
||||
*/
|
||||
fun buildCreatePublicKeyCredentialResponse(
|
||||
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters,
|
||||
userVerified: Boolean,
|
||||
backupEligibility: Boolean,
|
||||
backupState: Boolean
|
||||
): CreatePublicKeyCredentialResponse {
|
||||
@@ -518,7 +512,7 @@ object PasskeyHelper {
|
||||
keyTypeId = keyTypeId
|
||||
) ?: mapOf<Int, Any>()),
|
||||
userPresent = true,
|
||||
userVerified = true,
|
||||
userVerified = userVerified,
|
||||
backupEligibility = backupEligibility,
|
||||
backupState = backupState,
|
||||
publicKeyTypeId = keyTypeId,
|
||||
@@ -590,17 +584,18 @@ object PasskeyHelper {
|
||||
requestOptions: PublicKeyCredentialRequestOptions,
|
||||
clientDataResponse: ClientDataResponse,
|
||||
passkey: Passkey,
|
||||
backupEligibility: Boolean,
|
||||
backupState: Boolean
|
||||
userVerified: Boolean,
|
||||
defaultBackupEligibility: Boolean,
|
||||
defaultBackupState: Boolean
|
||||
): PublicKeyCredential {
|
||||
val getCredentialResponse = FidoPublicKeyCredential(
|
||||
id = passkey.credentialId,
|
||||
response = AuthenticatorAssertionResponse(
|
||||
requestOptions = requestOptions,
|
||||
userPresent = true,
|
||||
userVerified = true,
|
||||
backupEligibility = backupEligibility,
|
||||
backupState = backupState,
|
||||
userVerified = userVerified,
|
||||
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
|
||||
backupState = passkey.backupState ?: defaultBackupState,
|
||||
userHandle = passkey.userHandle,
|
||||
privateKey = passkey.privateKeyPem,
|
||||
clientDataResponse = clientDataResponse
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
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.retrieveAndRemoveEntries
|
||||
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.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.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 =
|
||||
CredentialState.LaunchGroupActivityForSelection(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
mCredentialUiState.value =
|
||||
CredentialState.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 entries = intent.retrieveAndRemoveEntries(database)
|
||||
val autofillComponent = mAutofillComponent
|
||||
if (autofillComponent == null)
|
||||
throw IOException("Autofill component is null")
|
||||
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 =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
)
|
||||
} else {
|
||||
mCredentialUiState.value = CredentialState.ShowError(
|
||||
RegisterInReadOnlyDatabaseException()
|
||||
)
|
||||
}
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
)
|
||||
} else {
|
||||
mCredentialUiState.value = CredentialState.ShowError(
|
||||
RegisterInReadOnlyDatabaseException()
|
||||
)
|
||||
}
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
mCredentialUiState.value =
|
||||
CredentialState.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 ShowAutofillSuggestionMessage: UIState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = AutofillLauncherViewModel::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
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<CredentialState>(CredentialState.Loading)
|
||||
val credentialUiState: StateFlow<CredentialState> = mCredentialUiState
|
||||
|
||||
fun showError(error: Throwable) {
|
||||
Log.e(TAG, "Error on credential provider launch", error)
|
||||
mCredentialUiState.value = CredentialState.ShowError(error)
|
||||
}
|
||||
|
||||
open fun onResult() {
|
||||
isResultLauncherRegistered = false
|
||||
mSelectionResult = null
|
||||
}
|
||||
|
||||
fun setResult(intent: Intent, lockDatabase: Boolean = false) {
|
||||
// Remove the launcher register
|
||||
onResult()
|
||||
mCredentialUiState.value = CredentialState.SetActivityResult(
|
||||
lockDatabase = lockDatabase,
|
||||
resultCode = RESULT_OK,
|
||||
data = intent
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelResult(lockDatabase: Boolean = false) {
|
||||
onResult()
|
||||
mCredentialUiState.value = CredentialState.SetActivityResult(
|
||||
lockDatabase = lockDatabase,
|
||||
resultCode = RESULT_CANCELED
|
||||
)
|
||||
}
|
||||
|
||||
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 CredentialState {
|
||||
object Loading : CredentialState()
|
||||
data class LaunchGroupActivityForSelection(
|
||||
val database: ContextualDatabase,
|
||||
val searchInfo: SearchInfo?,
|
||||
val typeMode: TypeMode
|
||||
): CredentialState()
|
||||
data class LaunchGroupActivityForRegistration(
|
||||
val database: ContextualDatabase,
|
||||
val registerInfo: RegisterInfo?,
|
||||
val typeMode: TypeMode
|
||||
): CredentialState()
|
||||
data class LaunchFileDatabaseSelectActivityForSelection(
|
||||
val searchInfo: SearchInfo?,
|
||||
val typeMode: TypeMode
|
||||
): CredentialState()
|
||||
data class LaunchFileDatabaseSelectActivityForRegistration(
|
||||
val registerInfo: RegisterInfo?,
|
||||
val typeMode: TypeMode
|
||||
): CredentialState()
|
||||
data class SetActivityResult(
|
||||
val lockDatabase: Boolean,
|
||||
val resultCode: Int,
|
||||
val data: Intent? = null
|
||||
): CredentialState()
|
||||
data class ShowError(
|
||||
val error: Throwable
|
||||
): CredentialState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = CredentialLauncherViewModel::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
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.core.net.toUri
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveAndRemoveEntries
|
||||
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.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.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||
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
|
||||
|
||||
class EntrySelectionViewModel(application: Application): CredentialLauncherViewModel(application) {
|
||||
|
||||
private var searchShareForMagikeyboard: Boolean = false
|
||||
private var mLockDatabaseAfterSelection: Boolean = false
|
||||
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||
val uiState: StateFlow<UIState> = mUiState
|
||||
|
||||
fun initialize() {
|
||||
searchShareForMagikeyboard = getApplication<Application>().isKeyboardActivatedInSettings()
|
||||
mLockDatabaseAfterSelection = false // TODO Close database after selection
|
||||
}
|
||||
|
||||
override fun launchActionIfNeeded(
|
||||
intent: Intent,
|
||||
specialMode: SpecialMode,
|
||||
database: ContextualDatabase?
|
||||
) {
|
||||
// Launch with database when a nodeId is present
|
||||
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
|
||||
super.launchActionIfNeeded(intent, specialMode, database)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun launchAction(
|
||||
intent: Intent,
|
||||
specialMode: SpecialMode,
|
||||
database: ContextualDatabase?
|
||||
) {
|
||||
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
|
||||
if (searchInfo != null) {
|
||||
launch(database, searchInfo)
|
||||
} else {
|
||||
// To manage share
|
||||
var sharedWebDomain: String? = null
|
||||
var otpString: String? = null
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
if ("text/plain" == intent.type) {
|
||||
// Retrieve web domain or OTP
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
else
|
||||
sharedWebDomain = extra.toUri().host
|
||||
}
|
||||
}
|
||||
launchSelection(database, sharedWebDomain, otpString)
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
// Retrieve OTP
|
||||
intent.dataString?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
}
|
||||
launchSelection(database, null, otpString)
|
||||
}
|
||||
else -> {
|
||||
if (database != null && database.loaded) {
|
||||
mUiState.value = UIState.LaunchGroupActivityForSearch(
|
||||
database = database,
|
||||
searchInfo = SearchInfo()
|
||||
)
|
||||
} else {
|
||||
mUiState.value = UIState.LaunchFileDatabaseSelectForSearch(
|
||||
searchInfo = SearchInfo()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------
|
||||
// Selection
|
||||
// -------------
|
||||
|
||||
private fun launchSelection(
|
||||
database: ContextualDatabase?,
|
||||
sharedWebDomain: String?,
|
||||
otpString: String?
|
||||
) {
|
||||
// Build domain search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
this.webDomain = sharedWebDomain
|
||||
this.otpString = otpString
|
||||
}
|
||||
launch(database, searchInfo)
|
||||
}
|
||||
|
||||
private fun launch(
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
// If database is open
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = getApplication(),
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
// Items found
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = searchInfo.toRegisterInfo(),
|
||||
typeMode = TypeMode.DEFAULT
|
||||
)
|
||||
} else {
|
||||
mCredentialUiState.value = CredentialState.ShowError(
|
||||
RegisterInReadOnlyDatabaseException()
|
||||
)
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
populateKeyboard(entryInfo)
|
||||
},
|
||||
{ autoSearch ->
|
||||
mCredentialUiState.value = CredentialState.LaunchGroupActivityForSelection(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.MAGIKEYBOARD
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
mUiState.value = UIState.LaunchGroupActivityForSearch(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = searchInfo.toRegisterInfo(),
|
||||
typeMode = TypeMode.DEFAULT
|
||||
)
|
||||
} else {
|
||||
mCredentialUiState.value = CredentialState.ShowError(
|
||||
RegisterInReadOnlyDatabaseException()
|
||||
)
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
mCredentialUiState.value = CredentialState.LaunchGroupActivityForSelection(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.MAGIKEYBOARD
|
||||
)
|
||||
} else {
|
||||
mUiState.value = UIState.LaunchGroupActivityForSearch(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
if (searchInfo.otpString != null) {
|
||||
mCredentialUiState.value = CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
|
||||
registerInfo = searchInfo.toRegisterInfo(),
|
||||
typeMode = TypeMode.DEFAULT
|
||||
)
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
mCredentialUiState.value = CredentialState.LaunchFileDatabaseSelectActivityForSelection(
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.MAGIKEYBOARD
|
||||
)
|
||||
} else {
|
||||
mUiState.value = UIState.LaunchFileDatabaseSelectForSearch(
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun populateKeyboard(entryInfo: EntryInfo) {
|
||||
// Automatically populate keyboard
|
||||
mUiState.value = UIState.PopulateKeyboard(entryInfo)
|
||||
setResult(Intent(), lockDatabase = mLockDatabaseAfterSelection)
|
||||
}
|
||||
|
||||
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 Magikeyboard", e)
|
||||
showError(e)
|
||||
}) {
|
||||
when (activityResult.resultCode) {
|
||||
RESULT_OK -> {
|
||||
withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Magikeyboard selection result")
|
||||
if (intent == null)
|
||||
throw IOException("Intent is null")
|
||||
val entries = intent.retrieveAndRemoveEntries(database)
|
||||
withContext(Dispatchers.Main) {
|
||||
// Populate Magikeyboard with entry
|
||||
entries.firstOrNull()?.let { entryInfo ->
|
||||
populateKeyboard(entryInfo)
|
||||
} // TODO Manage multiple entries in Magikeyboard
|
||||
}
|
||||
}
|
||||
}
|
||||
RESULT_CANCELED -> {
|
||||
withContext(Dispatchers.Main) {
|
||||
cancelResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun manageRegistrationResult(activityResult: ActivityResult) {
|
||||
super.manageRegistrationResult(activityResult)
|
||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||
Log.e(TAG, "Unable to create selection response for Magikeyboard", e)
|
||||
showError(e)
|
||||
}) {
|
||||
when (activityResult.resultCode) {
|
||||
RESULT_OK -> {
|
||||
// Empty data result
|
||||
// TODO Show Toast indicating value is saved
|
||||
withContext(Dispatchers.Main) {
|
||||
setResult(Intent(), lockDatabase = false)
|
||||
}
|
||||
}
|
||||
RESULT_CANCELED -> {
|
||||
withContext(Dispatchers.Main) {
|
||||
cancelResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UIState {
|
||||
object Loading: UIState()
|
||||
data class PopulateKeyboard(
|
||||
val entryInfo: EntryInfo
|
||||
): UIState()
|
||||
data class LaunchFileDatabaseSelectForSearch(
|
||||
val searchInfo: SearchInfo
|
||||
): UIState()
|
||||
data class LaunchGroupActivityForSearch(
|
||||
val database: ContextualDatabase,
|
||||
val searchInfo: SearchInfo
|
||||
): UIState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = EntrySelectionViewModel::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,23 @@ 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 mUserVerified: Boolean = true
|
||||
private var mBackupEligibility: Boolean = true
|
||||
private var mBackupState: Boolean = false
|
||||
private var mLockDatabase: Boolean = true
|
||||
|
||||
private var isResultLauncherRegistered: Boolean = false
|
||||
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||
val uiState: StateFlow<UIState> = mUiState
|
||||
|
||||
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||
val uiState: StateFlow<UIState> = _uiState
|
||||
|
||||
fun initialize() {
|
||||
fun initialize(userVerified: Boolean) {
|
||||
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
|
||||
mUserVerified = userVerified
|
||||
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
|
||||
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
|
||||
}
|
||||
@@ -79,19 +81,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 +104,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
context = getApplication(),
|
||||
privilegedApps = listOf(temptingApp)
|
||||
)
|
||||
launchPasskeyAction(
|
||||
launchAction(
|
||||
intent = intent,
|
||||
specialMode = specialMode,
|
||||
database = database
|
||||
@@ -139,54 +136,43 @@ 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(
|
||||
fun launchActionIfNeeded(
|
||||
userVerified: Boolean,
|
||||
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)
|
||||
this.mUserVerified = userVerified
|
||||
launchActionIfNeeded(intent, specialMode, database)
|
||||
}
|
||||
|
||||
override fun launchActionIfNeeded(
|
||||
intent: Intent,
|
||||
specialMode: SpecialMode,
|
||||
database: ContextualDatabase?
|
||||
) {
|
||||
// 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 +180,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 +249,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 =
|
||||
CredentialState.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 =
|
||||
CredentialState.LaunchFileDatabaseSelectActivityForSelection(
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -326,12 +319,13 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
appOrigin = appOrigin
|
||||
),
|
||||
passkey = passkey,
|
||||
backupEligibility = mBackupEligibility,
|
||||
backupState = mBackupState
|
||||
userVerified = mUserVerified,
|
||||
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 +334,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 +376,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
appOrigin = appOrigin
|
||||
),
|
||||
passkey = passkey,
|
||||
backupEligibility = mBackupEligibility,
|
||||
backupState = mBackupState
|
||||
userVerified = mUserVerified,
|
||||
defaultBackupEligibility = mBackupEligibility,
|
||||
defaultBackupState = mBackupState
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -389,7 +386,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
throw IOException("Usage parameters is null")
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
setResult(responseIntent)
|
||||
setResult(responseIntent, lockDatabase = mLockDatabaseAfterSelection)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,6 +414,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,7 +439,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
TAG, "Passkey found for registration, " +
|
||||
"but launch manual registration for a new entry"
|
||||
)
|
||||
_uiState.value = UIState.LaunchGroupActivityForRegistration(
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
@@ -448,7 +448,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
Log.d(TAG, "Launch new manual registration in opened database")
|
||||
_uiState.value = UIState.LaunchGroupActivityForRegistration(
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
@@ -456,8 +457,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
Log.d(TAG, "Manual passkey registration in closed database")
|
||||
_uiState.value =
|
||||
UIState.LaunchFileDatabaseSelectActivityForRegistration(
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
@@ -490,7 +491,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 +519,11 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
intent = responseIntent,
|
||||
response = buildCreatePublicKeyCredentialResponse(
|
||||
publicKeyCredentialCreationParameters = it,
|
||||
backupEligibility = mBackupEligibility,
|
||||
backupState = mBackupState
|
||||
userVerified = mUserVerified,
|
||||
backupEligibility = passkey?.backupEligibility
|
||||
?: mBackupEligibility,
|
||||
backupState = passkey?.backupState
|
||||
?: mBackupState
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -549,29 +553,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
|
||||
|
||||
@@ -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,122 +89,30 @@ 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() {
|
||||
fun onDatabaseChangeValidated() {
|
||||
mBinder?.getService()?.saveDatabaseInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private var databaseInfoListener = object :
|
||||
DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||
override fun onDatabaseInfoChanged(
|
||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnlyDatabase: Boolean
|
||||
) {
|
||||
activity?.let { activity ->
|
||||
activity.lifecycleScope.launch {
|
||||
if (databaseChangedDialogFragment == null) {
|
||||
databaseChangedDialogFragment = activity.supportFragmentManager
|
||||
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||
previousDatabaseInfo,
|
||||
newDatabaseInfo,
|
||||
readOnlyDatabase
|
||||
)
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
databaseChangedDialogFragment?.show(
|
||||
activity.supportFragmentManager,
|
||||
DATABASE_CHANGED_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
@@ -226,48 +120,17 @@ 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() {
|
||||
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 +153,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
|
||||
)
|
||||
@@ -363,63 +238,14 @@ class DatabaseTaskProvider(
|
||||
|
||||
try {
|
||||
context.unregisterReceiver(databaseTaskBroadcastReceiver)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// If receiver not register, do nothing
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +319,6 @@ class DatabaseTaskProvider(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
@@ -842,5 +667,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.StringRes
|
||||
|
||||
data class ProgressMessage(
|
||||
@StringRes
|
||||
var titleId: Int,
|
||||
var titleId: Int? = null,
|
||||
@StringRes
|
||||
var messageId: Int? = null,
|
||||
@StringRes
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,77 @@ 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
|
||||
searchOptions = optionsString()
|
||||
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 +130,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()) {
|
||||
&& !searchInfo.containsOnlyNullValues()
|
||||
) {
|
||||
searchInfo.getSearchParametersFromSearchInfo(context) { searchParameters ->
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
searchInfo,
|
||||
MAX_SEARCH_ENTRY
|
||||
searchParameters = searchParameters,
|
||||
max = MAX_SEARCH_ENTRY
|
||||
)?.let { searchGroup ->
|
||||
if (searchGroup.numberOfChildEntries > 0) {
|
||||
searchWithoutUI = true
|
||||
onItemsFound.invoke(database,
|
||||
searchGroup.getChildEntriesInfo(database))
|
||||
onItemsFound.invoke(
|
||||
database,
|
||||
searchGroup.getChildEntriesInfo(database)
|
||||
)
|
||||
} else
|
||||
onItemNotFound.invoke(database)
|
||||
} ?: onItemNotFound.invoke(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!searchWithoutUI) {
|
||||
} else
|
||||
onItemNotFound.invoke(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,8 @@ import java.util.*
|
||||
class PasswordGenerator(private val resources: Resources) {
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun generatePassword(length: Int,
|
||||
fun generatePassword(
|
||||
length: Int,
|
||||
upperCase: Boolean,
|
||||
lowerCase: Boolean,
|
||||
digits: Boolean,
|
||||
@@ -46,7 +47,8 @@ class PasswordGenerator(private val resources: Resources) {
|
||||
considerChars: String,
|
||||
ignoreChars: String,
|
||||
atLeastOneFromEach: Boolean,
|
||||
excludeAmbiguousChar: Boolean): String {
|
||||
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"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -164,7 +165,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
}
|
||||
}
|
||||
if (attachmentNotificationList.isEmpty()) {
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -62,7 +62,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
// Stop the service
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.ProgressMessage
|
||||
@@ -61,13 +62,13 @@ 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.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,
|
||||
@@ -261,11 +262,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
)
|
||||
}
|
||||
} else {
|
||||
/* Do not stopped here, service cannot be connected
|
||||
mActionTaskListeners.forEach { actionTaskListener ->
|
||||
actionTaskListener.onActionStopped(
|
||||
database
|
||||
)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,7 +339,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
val intentAction = intent?.action
|
||||
|
||||
if (intentAction == null && !database.loaded) {
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
val actionRunnable: ActionRunnable? = when (intentAction) {
|
||||
@@ -446,10 +448,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||
// Stop service after save if user remove task
|
||||
if (save && mTaskRemovedRequested) {
|
||||
actionOnLock()
|
||||
stopService()
|
||||
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
|
||||
if (!database.loaded) {
|
||||
stopSelf()
|
||||
stopService()
|
||||
} else {
|
||||
// Restart the service to open lock notification
|
||||
try {
|
||||
@@ -534,11 +536,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
|
||||
val notificationBuilder = buildNewNotification().apply {
|
||||
setSmallIcon(iconId)
|
||||
intent?.let {
|
||||
setContentTitle(getString(
|
||||
intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId))
|
||||
)
|
||||
}
|
||||
val titleId = mProgressMessage.titleId?.let {
|
||||
intent?.getIntExtra(DATABASE_TASK_TITLE_KEY, it)
|
||||
} ?: R.string.app_name
|
||||
setContentTitle(getString(titleId))
|
||||
setAutoCancel(false)
|
||||
setContentIntent(null)
|
||||
}
|
||||
@@ -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
|
||||
@@ -660,7 +661,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
|
||||
private fun updateMessage(resId: Int) {
|
||||
mProgressMessage.messageId = resId
|
||||
mProgressMessage = mProgressMessage.copy(
|
||||
messageId = resId
|
||||
)
|
||||
notifyProgressMessage()
|
||||
}
|
||||
|
||||
@@ -672,13 +675,19 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
updateMessage(R.string.decrypting_db)
|
||||
}
|
||||
|
||||
override fun actionOnLock() {
|
||||
override fun stopService() {
|
||||
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
|
||||
super.actionOnLock()
|
||||
super.stopService()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,9 +718,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 {
|
||||
@@ -722,7 +731,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
// Close channels
|
||||
closeChallengeResponse()
|
||||
// Restore previous message
|
||||
mProgressMessage = previousMessage
|
||||
mProgressMessage = previousMessage.apply {
|
||||
cancelable = null
|
||||
}
|
||||
notifyProgressMessage()
|
||||
}
|
||||
return response
|
||||
@@ -906,7 +917,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
|
||||
private fun buildDatabaseAssignCredentialActionTask(
|
||||
intent: Intent,
|
||||
database: ContextualDatabase,
|
||||
database: ContextualDatabase
|
||||
): ActionRunnable? {
|
||||
return if (intent.hasExtra(DATABASE_URI_KEY)
|
||||
&& intent.hasExtra(MAIN_CREDENTIAL_KEY)
|
||||
|
||||
@@ -55,7 +55,7 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
// Stop the service
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.services
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.app.ServiceCompat
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LockReceiver
|
||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||
@@ -29,13 +28,7 @@ import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
||||
abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
private var mLockReceiver: LockReceiver = LockReceiver {
|
||||
actionOnLock()
|
||||
}
|
||||
|
||||
protected open fun actionOnLock() {
|
||||
// Stop the service in all cases
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -46,7 +39,7 @@ abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
if (!TimeoutHelper.temporarilyDisableLock) {
|
||||
actionOnLock()
|
||||
stopService()
|
||||
}
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.util.TypedValue
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
@@ -114,6 +115,12 @@ abstract class NotificationService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun stopService() {
|
||||
// Stop the service in all cases
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
protected fun defineTimerJob(builder: NotificationCompat.Builder,
|
||||
type: NotificationServiceType,
|
||||
timeoutMilliseconds: Long,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,6 +13,8 @@ abstract class ExternalSettingsActivity : DatabaseModeActivity() {
|
||||
|
||||
private var lockView: FloatingActionButton? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -302,7 +302,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
biometricUnlockEnablePreference.isChecked = false
|
||||
warningMessage(activity, keystoreWarning = true, deleteKeys = true) {
|
||||
biometricUnlockEnablePreference.isChecked = true
|
||||
deviceCredentialUnlockEnablePreference?.isChecked = false
|
||||
deviceCredentialUnlockEnablePreference.isChecked = false
|
||||
}
|
||||
} else {
|
||||
biometricUnlockEnablePreference.isChecked = false
|
||||
@@ -349,7 +349,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
deviceCredentialUnlockEnablePreference.isChecked = false
|
||||
warningMessage(activity, keystoreWarning = true, deleteKeys = true) {
|
||||
deviceCredentialUnlockEnablePreference.isChecked = true
|
||||
biometricUnlockEnablePreference?.isChecked = false
|
||||
biometricUnlockEnablePreference.isChecked = false
|
||||
}
|
||||
} else {
|
||||
deviceCredentialUnlockEnablePreference.isChecked = false
|
||||
@@ -412,7 +412,6 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
warningAlertDialog = AlertDialog.Builder(activity)
|
||||
.setMessage(message)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setPositiveButton(resources.getString(android.R.string.ok)
|
||||
) { _, _ ->
|
||||
validate?.invoke()
|
||||
@@ -524,27 +523,23 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
|
||||
var otherDialogFragment = false
|
||||
|
||||
var dialogFragment: DialogFragment? = null
|
||||
// Main Preferences
|
||||
|
||||
when (preference.key) {
|
||||
getString(R.string.app_timeout_key),
|
||||
getString(R.string.clipboard_timeout_key),
|
||||
getString(R.string.temp_device_unlock_timeout_key) -> {
|
||||
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
else -> otherDialogFragment = true
|
||||
else -> {}
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
@Suppress("DEPRECATION")
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||
}
|
||||
} else {
|
||||
// Could not be handled here. Try with the super method.
|
||||
else if (otherDialogFragment) {
|
||||
super.onDisplayPreferenceDialog(preference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -34,24 +42,53 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
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 com.kunzisoft.keepass.viewmodels.SettingsViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
|
||||
|
||||
private val mSettingsViewModel: SettingsViewModel by activityViewModels()
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabase: ContextualDatabase? = null
|
||||
private val mUserVerificationViewModel: UserVerificationViewModel by activityViewModels()
|
||||
|
||||
private val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
private var mDatabaseReadOnly: Boolean = false
|
||||
private var mMergeDataAllowed: Boolean = false
|
||||
private var mDatabaseAutoSaveEnabled: Boolean = true
|
||||
@@ -114,19 +151,91 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
|
||||
onDatabaseActionFinished(it.database, it.actionTask, it.result)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mUserVerificationViewModel.userVerificationState.collect { state ->
|
||||
when (state) {
|
||||
is UserVerificationViewModel.UVState.Loading -> {}
|
||||
is UserVerificationViewModel.UVState.OnUserVerificationCanceled -> {
|
||||
mSettingsViewModel.showError(state.error)
|
||||
mUserVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
is UserVerificationViewModel.UVState.OnUserVerificationSucceeded -> {
|
||||
val data = state.dataToVerify
|
||||
when (data.actionType) {
|
||||
UserVerificationActionType.EDIT_DATABASE_SETTING -> {
|
||||
val database = data.database
|
||||
val preferenceKey = data.preferenceKey
|
||||
if (database != null && preferenceKey != null) {
|
||||
// Main Preferences
|
||||
when (preferenceKey) {
|
||||
// Master Key
|
||||
getString(R.string.settings_database_change_credentials_key) -> {
|
||||
SetMainCredentialDialogFragment
|
||||
.getInstance(database.allowNoMasterKey)
|
||||
.show(parentFragmentManager, "passwordDialog")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
// TODO Settings in compose
|
||||
@Suppress("DEPRECATION")
|
||||
mSettingsViewModel.dialogFragment?.let { dialogFragment ->
|
||||
dialogFragment.setTargetFragment(
|
||||
this@NestedDatabaseSettingsFragment, 0
|
||||
)
|
||||
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||
}
|
||||
mSettingsViewModel.dialogFragment = null
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
mUserVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
view.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||
context = requireContext(),
|
||||
databaseLoaded = database?.loaded
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,22 +276,20 @@ 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) {
|
||||
if (database.loaded) {
|
||||
when (mScreen) {
|
||||
Screen.DATABASE -> {
|
||||
onCreateDatabasePreference(it)
|
||||
onCreateDatabasePreference(database)
|
||||
}
|
||||
Screen.DATABASE_SECURITY -> {
|
||||
onCreateDatabaseSecurityPreference(it)
|
||||
onCreateDatabaseSecurityPreference(database)
|
||||
}
|
||||
Screen.DATABASE_MASTER_KEY -> {
|
||||
onCreateDatabaseMasterKeyPreference(it)
|
||||
onCreateDatabaseMasterKeyPreference(database)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
@@ -191,7 +298,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
Log.e(javaClass.name, "Database isn't ready")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCreateDatabasePreference(database: ContextualDatabase) {
|
||||
val dbGeneralPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_general_key))
|
||||
@@ -272,7 +378,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
// Change the recycle bin group
|
||||
recycleBinGroupPref?.setOnPreferenceClickListener {
|
||||
|
||||
true
|
||||
}
|
||||
// Recycle Bin group
|
||||
@@ -378,11 +483,18 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
|
||||
private fun onCreateDatabaseMasterKeyPreference(database: ContextualDatabase) {
|
||||
findPreference<Preference>(getString(R.string.settings_database_change_credentials_key))?.apply {
|
||||
val changeCredentialKey = getString(R.string.settings_database_change_credentials_key)
|
||||
findPreference<Preference>(changeCredentialKey)?.apply {
|
||||
isEnabled = if (!mDatabaseReadOnly) {
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
SetMainCredentialDialogFragment.getInstance(database.allowNoMasterKey)
|
||||
.show(parentFragmentManager, "passwordDialog")
|
||||
checkUserVerification(
|
||||
mUserVerificationViewModel,
|
||||
UserVerificationData(
|
||||
actionType = UserVerificationActionType.EDIT_DATABASE_SETTING,
|
||||
database = database,
|
||||
preferenceKey = changeCredentialKey
|
||||
)
|
||||
)
|
||||
false
|
||||
}
|
||||
true
|
||||
@@ -409,7 +521,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
// To reassign color listener after orientation change
|
||||
val chromaDialog = parentFragmentManager.findFragmentByTag(TAG_PREF_FRAGMENT) as DatabaseColorPreferenceDialogFragmentCompat?
|
||||
chromaDialog?.onColorSelectedListener = colorSelectedListener
|
||||
} catch (e: Exception) {}
|
||||
} catch (_: Exception) {}
|
||||
|
||||
return view
|
||||
}
|
||||
@@ -458,7 +570,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newDefaultUsername
|
||||
} else {
|
||||
mDatabase?.defaultUsername = oldDefaultUsername
|
||||
database.defaultUsername = oldDefaultUsername
|
||||
oldDefaultUsername
|
||||
}
|
||||
dbDefaultUsernamePref?.summary = defaultUsernameToShow
|
||||
@@ -471,7 +583,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 +595,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newCompression
|
||||
} else {
|
||||
mDatabase?.compressionAlgorithm = oldCompression
|
||||
database.compressionAlgorithm = oldCompression
|
||||
oldCompression
|
||||
}
|
||||
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
|
||||
@@ -497,7 +609,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
} else {
|
||||
oldRecycleBin
|
||||
}
|
||||
mDatabase?.setRecycleBin(recycleBinToShow)
|
||||
database.setRecycleBin(recycleBinToShow)
|
||||
refreshRecycleBinGroup(database)
|
||||
}
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
|
||||
@@ -509,7 +621,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 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newMaxHistoryItems
|
||||
} else {
|
||||
mDatabase?.historyMaxItems = oldMaxHistoryItems
|
||||
database.historyMaxItems = oldMaxHistoryItems
|
||||
oldMaxHistoryItems
|
||||
}
|
||||
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
|
||||
@@ -531,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newMaxHistorySize
|
||||
} else {
|
||||
mDatabase?.historyMaxSize = oldMaxHistorySize
|
||||
database.historyMaxSize = oldMaxHistorySize
|
||||
oldMaxHistorySize
|
||||
}
|
||||
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
|
||||
@@ -549,7 +661,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newEncryption
|
||||
} else {
|
||||
mDatabase?.encryptionAlgorithm = oldEncryption
|
||||
database.encryptionAlgorithm = oldEncryption
|
||||
oldEncryption
|
||||
}
|
||||
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
|
||||
@@ -561,7 +673,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newKeyDerivationEngine
|
||||
} else {
|
||||
mDatabase?.kdfEngine = oldKeyDerivationEngine
|
||||
database.kdfEngine = oldKeyDerivationEngine
|
||||
oldKeyDerivationEngine
|
||||
}
|
||||
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
|
||||
@@ -578,7 +690,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newIterations
|
||||
} else {
|
||||
mDatabase?.numberKeyEncryptionRounds = oldIterations
|
||||
database.numberKeyEncryptionRounds = oldIterations
|
||||
oldIterations
|
||||
}
|
||||
mRoundPref?.summary = roundsToShow.toString()
|
||||
@@ -590,7 +702,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newMemoryUsage
|
||||
} else {
|
||||
mDatabase?.memoryUsage = oldMemoryUsage
|
||||
database.memoryUsage = oldMemoryUsage
|
||||
oldMemoryUsage
|
||||
}
|
||||
mMemoryPref?.summary = memoryToShow.toString()
|
||||
@@ -602,7 +714,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newParallelism
|
||||
} else {
|
||||
mDatabase?.parallelism = oldParallelism
|
||||
database.parallelism = oldParallelism
|
||||
oldParallelism
|
||||
}
|
||||
mParallelismPref?.summary = parallelismToShow.toString()
|
||||
@@ -677,9 +789,15 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
|
||||
if (dialogFragment != null && !mDatabaseReadOnly) {
|
||||
@Suppress("DEPRECATION")
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||
mSettingsViewModel.dialogFragment = dialogFragment
|
||||
checkUserVerification(
|
||||
mUserVerificationViewModel,
|
||||
UserVerificationData(
|
||||
actionType = UserVerificationActionType.EDIT_DATABASE_SETTING,
|
||||
database = mDatabase,
|
||||
preferenceKey = preference.key
|
||||
)
|
||||
)
|
||||
}
|
||||
// Could not be handled here. Try with the super method.
|
||||
else if (otherDialogFragment) {
|
||||
@@ -689,7 +807,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
context?.let { context ->
|
||||
mDatabaseAutoSaveEnabled = PreferencesUtil.isAutoSaveDatabaseEnabled(context)
|
||||
}
|
||||
|
||||
@@ -30,11 +30,17 @@ import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFra
|
||||
abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
enum class Screen {
|
||||
APPLICATION, FORM_FILLING, DEVICE_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
|
||||
APPLICATION,
|
||||
FORM_FILLING,
|
||||
DEVICE_UNLOCK,
|
||||
APPEARANCE,
|
||||
DATABASE,
|
||||
DATABASE_SECURITY,
|
||||
DATABASE_MASTER_KEY
|
||||
}
|
||||
|
||||
fun getScreen(): Screen {
|
||||
return Screen.values()[requireArguments().getInt(TAG_KEY)]
|
||||
return Screen.entries[requireArguments().getInt(TAG_KEY)]
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@@ -50,8 +56,7 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
|
||||
preferenceInDev.setOnPreferenceClickListener { preference ->
|
||||
try { // don't check if we can
|
||||
(preference as TwoStatePreference).isChecked = false
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
UnderDevelopmentFeatureDialogFragment().show(parentFragmentManager, "underDevFeatureDialog")
|
||||
false
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
@@ -132,12 +132,6 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.hide_templates_default))
|
||||
}
|
||||
|
||||
fun hideProtectedValue(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.hide_password_key),
|
||||
context.resources.getBoolean(R.bool.hide_password_default))
|
||||
}
|
||||
|
||||
fun colorizePassword(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.colorize_password_key),
|
||||
@@ -352,6 +346,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 +385,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 +684,24 @@ 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 isUserVerificationDeviceCredential(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.user_verification_device_credential_key),
|
||||
context.resources.getBoolean(R.bool.user_verification_device_credential_default))
|
||||
}
|
||||
|
||||
fun isUserVerificationPreferred(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.user_verification_preferred_key),
|
||||
context.resources.getBoolean(R.bool.user_verification_preferred_default))
|
||||
}
|
||||
|
||||
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
|
||||
@@ -854,6 +870,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())
|
||||
@@ -868,7 +888,6 @@ object PreferencesUtil {
|
||||
context.getString(R.string.show_entry_colors_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.hide_templates_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.colorize_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.list_entries_show_username_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.list_groups_show_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
|
||||
@@ -28,9 +28,13 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||
@@ -41,6 +45,9 @@ import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.view.showError
|
||||
import com.kunzisoft.keepass.viewmodels.SettingsViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.joda.time.DateTime
|
||||
import java.util.Properties
|
||||
|
||||
@@ -49,6 +56,8 @@ open class SettingsActivity
|
||||
MainPreferenceFragment.Callback,
|
||||
SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
|
||||
|
||||
private val mSettingsViewModel: SettingsViewModel by viewModels()
|
||||
|
||||
private var backupManager: BackupManager? = null
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
@@ -70,8 +79,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)
|
||||
@@ -114,7 +127,7 @@ open class SettingsActivity
|
||||
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
|
||||
toolbar?.setTitle(R.string.settings)
|
||||
else
|
||||
toolbar?.title = savedInstanceState?.getString(TITLE_KEY)
|
||||
toolbar?.title = savedInstanceState.getString(TITLE_KEY)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
@@ -141,6 +154,20 @@ open class SettingsActivity
|
||||
// Eat state
|
||||
intent.removeExtra(FRAGMENT_ARG)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mSettingsViewModel.settingsState.collect { settingsState ->
|
||||
when (settingsState) {
|
||||
is SettingsViewModel.SettingsState.Wait -> {}
|
||||
is SettingsViewModel.SettingsState.ShowError -> {
|
||||
coordinatorLayout?.showError(settingsState.error)
|
||||
mSettingsViewModel.errorShown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,10 +182,6 @@ open class SettingsActivity
|
||||
return coordinatorLayout
|
||||
}
|
||||
|
||||
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
@@ -188,7 +211,7 @@ open class SettingsActivity
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||
assignPassword(mainCredential)
|
||||
assignMainCredential(mainCredential)
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||
|
||||
@@ -95,11 +95,8 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
database?.let {
|
||||
var initColor = it.customColor
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
var initColor = database.customColor
|
||||
if (initColor != null) {
|
||||
enableSwitchView.isChecked = true
|
||||
} else {
|
||||
@@ -109,7 +106,6 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
|
||||
chromaColorView.currentColor = initColor
|
||||
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
super.onDialogClosed(database, positiveResult)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -51,13 +51,10 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
algorithmSelected = database.encryptionAlgorithm
|
||||
mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
super.onDialogClosed(database, positiveResult)
|
||||
|
||||
@@ -54,12 +54,12 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
kdfEngineSelected = database.kdfEngine
|
||||
mKdfAdapter?.setItems(database.availableKdfEngines, kdfEngineSelected)
|
||||
}
|
||||
mKdfAdapter?.setItems(
|
||||
items = database.availableKdfEngines,
|
||||
itemUsed = kdfEngineSelected
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -31,9 +31,8 @@ class DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePrefer
|
||||
setExplanationText(R.string.max_history_items_summary)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.historyMaxItems?.let { maxItemsDatabase ->
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
val maxItemsDatabase = database.historyMaxItems
|
||||
inputText = maxItemsDatabase.toString()
|
||||
setSwitchAction({ isChecked ->
|
||||
inputText = if (!isChecked) {
|
||||
@@ -44,7 +43,6 @@ class DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePrefer
|
||||
showInputText(isChecked)
|
||||
}, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
super.onDialogClosed(database, positiveResult)
|
||||
|
||||
@@ -34,9 +34,8 @@ class DatabaseMaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePrefere
|
||||
setExplanationText(R.string.max_history_size_summary)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.historyMaxSize?.let { maxItemsDatabase ->
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
val maxItemsDatabase = database.historyMaxSize
|
||||
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
|
||||
.toBetterByteFormat()
|
||||
inputText = dataByte.number.toString()
|
||||
@@ -59,7 +58,6 @@ class DatabaseMaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePrefere
|
||||
showInputText(isChecked)
|
||||
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
super.onDialogClosed(database, positiveResult)
|
||||
|
||||
@@ -34,16 +34,13 @@ class DatabaseMemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreference
|
||||
setExplanationText(R.string.memory_usage_explanation)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
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) {
|
||||
if (positiveResult) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -48,13 +48,10 @@ class DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
mGroupRecycleBin = database.recycleBin
|
||||
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemSelected(item: Group) {
|
||||
mGroupRecycleBin = item
|
||||
|
||||
@@ -46,6 +46,8 @@ class DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat : DatabaseSavePre
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ->
|
||||
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,
|
||||
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 {
|
||||
|
||||
@@ -48,13 +48,10 @@ class DatabaseTemplatesGroupPreferenceDialogFragmentCompat
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
mGroupTemplates = database.templatesGroup
|
||||
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemSelected(item: Group) {
|
||||
mGroupTemplates = item
|
||||
|
||||
@@ -27,32 +27,27 @@ import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
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.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class ProgressTaskDialogFragment : DialogFragment() {
|
||||
|
||||
@StringRes
|
||||
private var title = UNDEFINED
|
||||
@StringRes
|
||||
private var message = UNDEFINED
|
||||
@StringRes
|
||||
private var warning = UNDEFINED
|
||||
private var cancellable: (() -> Unit)? = null
|
||||
|
||||
private var titleView: TextView? = null
|
||||
private var messageView: TextView? = null
|
||||
private var warningView: TextView? = null
|
||||
private var cancelButton: Button? = null
|
||||
private var progressView: ProgressBar? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
private val progressTaskViewModel: ProgressTaskViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
try {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
@@ -71,68 +66,63 @@ open class ProgressTaskDialogFragment : DialogFragment() {
|
||||
cancelButton = root.findViewById(R.id.progress_dialog_cancel)
|
||||
progressView = root.findViewById(R.id.progress_dialog_bar)
|
||||
|
||||
updateTitle(title)
|
||||
updateMessage(message)
|
||||
updateWarning(warning)
|
||||
setCancellable(cancellable)
|
||||
|
||||
isCancelable = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
progressTaskViewModel.progressTaskState.collect { state ->
|
||||
when (state) {
|
||||
is ProgressTaskViewModel.ProgressTaskState.Show -> {
|
||||
val value = state.value
|
||||
updateView(
|
||||
titleView,
|
||||
value.titleId?.let { title ->
|
||||
getString(title)
|
||||
})
|
||||
updateView(
|
||||
messageView,
|
||||
value.messageId?.let { message ->
|
||||
getString(message)
|
||||
})
|
||||
updateView(
|
||||
warningView,
|
||||
value.warningId?.let { warning ->
|
||||
getString(warning)
|
||||
})
|
||||
cancelButton?.apply {
|
||||
isVisible = value.cancelable != null
|
||||
setOnClickListener {
|
||||
value.cancelable?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Nothing here, this fragment is stopped externally
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create progress dialog")
|
||||
Log.e(TAG, "Unable to create progress dialog", e)
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
fun setTitle(@StringRes titleId: Int) {
|
||||
this.title = titleId
|
||||
}
|
||||
|
||||
private fun updateView(textView: TextView?, @StringRes resId: Int) {
|
||||
activity?.lifecycleScope?.launch {
|
||||
if (resId == UNDEFINED) {
|
||||
private fun updateView(textView: TextView?, value: String?) {
|
||||
if (value == null) {
|
||||
textView?.visibility = View.GONE
|
||||
} else {
|
||||
textView?.setText(resId)
|
||||
textView?.text = value
|
||||
textView?.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCancelable() {
|
||||
activity?.lifecycleScope?.launch {
|
||||
cancelButton?.isVisible = cancellable != null
|
||||
cancelButton?.setOnClickListener {
|
||||
cancellable?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle(@StringRes resId: Int?) {
|
||||
this.title = resId ?: UNDEFINED
|
||||
updateView(titleView, title)
|
||||
}
|
||||
|
||||
fun updateMessage(@StringRes resId: Int?) {
|
||||
this.message = resId ?: UNDEFINED
|
||||
updateView(messageView, message)
|
||||
}
|
||||
|
||||
fun updateWarning(@StringRes resId: Int?) {
|
||||
this.warning = resId ?: UNDEFINED
|
||||
updateView(warningView, warning)
|
||||
}
|
||||
|
||||
fun setCancellable(cancellable: (() -> Unit)?) {
|
||||
this.cancellable = cancellable
|
||||
updateCancelable()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ProgressTaskDialogFragment::class.java.simpleName
|
||||
const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment"
|
||||
const val UNDEFINED = -1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.kunzisoft.keepass.tasks
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.kunzisoft.keepass.database.ProgressMessage
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class ProgressTaskViewModel: ViewModel() {
|
||||
|
||||
private val mProgressTaskState = MutableStateFlow<ProgressTaskState>(ProgressTaskState.Hide)
|
||||
val progressTaskState: StateFlow<ProgressTaskState> = mProgressTaskState
|
||||
|
||||
fun show(value: ProgressMessage) {
|
||||
mProgressTaskState.update { currentState ->
|
||||
ProgressTaskState.Show(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
mProgressTaskState.value = ProgressTaskState.Hide
|
||||
}
|
||||
|
||||
sealed class ProgressTaskState {
|
||||
data class Show(val value: ProgressMessage): ProgressTaskState()
|
||||
object Hide: ProgressTaskState()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -123,25 +98,43 @@ object AppUtil {
|
||||
}
|
||||
|
||||
val processedPackageNames = mutableSetOf<String>()
|
||||
|
||||
for (resolveInfo in resolveInfoList) {
|
||||
val packageName = resolveInfo.activityInfo.packageName
|
||||
if (packageName != null && !processedPackageNames.contains(packageName)) {
|
||||
try {
|
||||
buildAndroidPrivilegedApp(packageManager, packageName)?.let { privilegedApp ->
|
||||
browserList.add(privilegedApp)
|
||||
processedPackageNames.add(packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the Play Service
|
||||
val gServices = "com.google.android.gms"
|
||||
buildAndroidPrivilegedApp(packageManager, gServices)?.let { privilegedApp ->
|
||||
browserList.add(privilegedApp)
|
||||
processedPackageNames.add(gServices)
|
||||
}
|
||||
|
||||
return browserList.distinctBy { it.packageName } // Ensure uniqueness just in case
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
private fun buildAndroidPrivilegedApp(
|
||||
packageManager: PackageManager,
|
||||
packageName: String
|
||||
): AndroidPrivilegedApp? {
|
||||
return try {
|
||||
val packageInfo = packageManager.getPackageInfo(
|
||||
packageName,
|
||||
PackageManager.GET_SIGNING_CERTIFICATES
|
||||
)
|
||||
val signatureFingerprints = packageInfo.signingInfo?.getAllFingerprints()
|
||||
signatureFingerprints?.let {
|
||||
browserList.add(AndroidPrivilegedApp(packageName, signatureFingerprints))
|
||||
processedPackageNames.add(packageName)
|
||||
AndroidPrivilegedApp(packageName, signatureFingerprints)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppUtil::class.simpleName, "Error processing package: $packageName", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
return browserList.distinctBy { it.packageName } // Ensure uniqueness just in case
|
||||
}
|
||||
}
|
||||
@@ -19,22 +19,31 @@
|
||||
*/
|
||||
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 {
|
||||
exceptionHandler?.let {
|
||||
action.invoke()
|
||||
} ?: try {
|
||||
action.invoke()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -42,7 +51,7 @@ class IOActionTask<T>(
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
afterActionListener?.invoke(asyncResult.await())
|
||||
onActionComplete?.invoke(asyncResult.await())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Editable
|
||||
import android.text.InputType
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextWatcher
|
||||
@@ -51,7 +50,6 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
|
||||
|
||||
private var mViewHint: String = ""
|
||||
private var mMaxLines: Int = 3
|
||||
private var mShowPassword: Boolean = false
|
||||
|
||||
private var mPasswordTextWatchers: MutableList<TextWatcher> = mutableListOf()
|
||||
private var mPasswordTextWatcher: TextWatcher? = null
|
||||
@@ -65,8 +63,6 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
|
||||
mViewHint = getString(R.styleable.PasswordView_passwordHint)
|
||||
?: context.getString(R.string.password)
|
||||
mMaxLines = getInteger(R.styleable.PasswordView_passwordMaxLines, mMaxLines)
|
||||
mShowPassword = getBoolean(R.styleable.PasswordView_passwordVisible,
|
||||
!PreferencesUtil.hideProtectedValue(context))
|
||||
} finally {
|
||||
recycle()
|
||||
}
|
||||
@@ -76,16 +72,12 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
|
||||
inflater?.inflate(R.layout.view_password_edit, this)
|
||||
|
||||
passwordInputLayout = findViewById(R.id.password_edit_input_layout)
|
||||
passwordInputLayout?.hint = mViewHint
|
||||
passwordInputLayout.hint = mViewHint
|
||||
passwordText = findViewById(R.id.password_edit_text)
|
||||
if (mShowPassword) {
|
||||
passwordText?.inputType = passwordText.inputType or
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
}
|
||||
passwordText?.maxLines = mMaxLines
|
||||
passwordText?.applyFontVisibility()
|
||||
passwordText.maxLines = mMaxLines
|
||||
passwordText.applyFontVisibility()
|
||||
passwordStrengthProgress = findViewById(R.id.password_edit_strength_progress)
|
||||
passwordStrengthProgress?.apply {
|
||||
passwordStrengthProgress.apply {
|
||||
setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
|
||||
progress = 0
|
||||
max = 100
|
||||
@@ -93,7 +85,7 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
|
||||
passwordEntropy = findViewById(R.id.password_edit_entropy)
|
||||
|
||||
mPasswordEntropyCalculator = PasswordEntropy {
|
||||
passwordText?.text?.toString()?.let { firstPassword ->
|
||||
passwordText.text?.toString()?.let { firstPassword ->
|
||||
getEntropyStrength(firstPassword)
|
||||
}
|
||||
}
|
||||
@@ -119,7 +111,7 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
|
||||
PasswordGenerator.colorizedPassword(editable)
|
||||
}
|
||||
}
|
||||
passwordText?.addTextChangedListener(mPasswordTextWatcher)
|
||||
passwordText.addTextChangedListener(mPasswordTextWatcher)
|
||||
}
|
||||
|
||||
private fun getEntropyStrength(passwordText: String) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user