mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
237 Commits
feature/Pa
...
4.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
cae69e7572 | ||
|
|
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.
|
description: Report a bug.
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
|
|||||||
27
CHANGELOG
27
CHANGELOG
@@ -1,5 +1,30 @@
|
|||||||
|
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)
|
KeePassDX(4.2.0)
|
||||||
* Passkeys management #1421 #2097 (Thx @cali-95)
|
* Passkeys management #1421 #2097 (@cali-95)
|
||||||
|
* Confirm usage of passkey #2165 #2124
|
||||||
|
* Dialog to manage missing signature #2152 #2155 #2161 #2160
|
||||||
|
* Capture error #2159 #2215
|
||||||
|
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
|
||||||
|
* Search settings #2112 #2181 #2187 #2204
|
||||||
|
* Autofill refactoring #765 #2196
|
||||||
|
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
|
||||||
|
|
||||||
|
KeePassDX(4.1.9)
|
||||||
|
* Fix landscape UI #2198 #2200 (@chenxiaolong)
|
||||||
|
* Fix start loop and flash screen #2201
|
||||||
|
* Small fixes
|
||||||
|
|
||||||
KeePassDX(4.1.8)
|
KeePassDX(4.1.8)
|
||||||
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -6,19 +6,21 @@
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Create database files / entries and groups.
|
- **Passkeys** for authentication and **local storage of private keys**.
|
||||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
- **Biometric recognition** for fast unlocking (fingerprint / face unlock / …).
|
||||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
- **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**.
|
- Allows opening and **copying URI / URL fields quickly**.
|
||||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
- Dynamic **templates** for each type of entry.
|
||||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
|
||||||
- Material design with **themes**.
|
|
||||||
- **Auto-Fill** and Integration.
|
|
||||||
- Field filling **keyboard**.
|
|
||||||
- Dynamic **templates**
|
|
||||||
- **History** of each entry.
|
- **History** of each entry.
|
||||||
- Precise management of **settings**.
|
- 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**.
|
KeePassDX is **open source** and **ad-free**.
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 35
|
targetSdkVersion 35
|
||||||
versionCode = 142
|
versionCode = 147
|
||||||
versionName = "4.2.0beta02"
|
versionName = "4.2.2"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
|
|||||||
@@ -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",
|
"type": "android",
|
||||||
"info": {
|
"info": {
|
||||||
|
|||||||
@@ -178,18 +178,22 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
|
||||||
android:theme="@style/Theme.Transparent" />
|
android:theme="@style/Theme.Transparent"
|
||||||
|
android:exported="false"
|
||||||
|
android:excludeFromRecents="true" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:excludeFromRecents="true"/>
|
android:exported="false"
|
||||||
|
android:excludeFromRecents="true" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:excludeFromRecents="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -208,8 +212,8 @@
|
|||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
tools:targetApi="upside_down_cake" />
|
tools:targetApi="upside_down_cake" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
|
|||||||
@@ -79,11 +79,13 @@ import com.kunzisoft.keepass.view.hideByFading
|
|||||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
|
import java.util.EnumSet
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class EntryActivity : DatabaseLockActivity() {
|
class EntryActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
private var footer: ViewGroup? = null
|
private var footer: ViewGroup? = null
|
||||||
|
private var container: View? = null
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||||
private var appBarLayout: AppBarLayout? = null
|
private var appBarLayout: AppBarLayout? = null
|
||||||
@@ -123,6 +125,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
private var mBackgroundColor: Int? = null
|
private var mBackgroundColor: Int? = null
|
||||||
private var mForegroundColor: Int? = null
|
private var mForegroundColor: Int? = null
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -135,6 +139,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
// Get views
|
// Get views
|
||||||
footer = findViewById(R.id.activity_entry_footer)
|
footer = findViewById(R.id.activity_entry_footer)
|
||||||
|
container = findViewById(R.id.activity_entry_container)
|
||||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||||
appBarLayout = findViewById(R.id.app_bar)
|
appBarLayout = findViewById(R.id.app_bar)
|
||||||
@@ -150,8 +155,12 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
setTransparentNavigationBar {
|
setTransparentNavigationBar {
|
||||||
// To fix margin with API 27
|
// To fix margin with API 27
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
||||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
container?.applyWindowInsets(EnumSet.of(
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
WindowInsetPosition.TOP_MARGINS,
|
||||||
|
WindowInsetPosition.BOTTOM_MARGINS,
|
||||||
|
WindowInsetPosition.START_MARGINS,
|
||||||
|
WindowInsetPosition.END_MARGINS,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty title
|
// Empty title
|
||||||
@@ -305,11 +314,11 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
launch(
|
launch(
|
||||||
this,
|
activity = this,
|
||||||
database,
|
database = database,
|
||||||
historySelected.nodeId,
|
entryId = historySelected.nodeId,
|
||||||
historySelected.historyPosition,
|
historyPosition = historySelected.historyPosition,
|
||||||
mEntryActivityResultLauncher
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,9 +332,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
return coordinatorLayout
|
return coordinatorLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mEntryViewModel.loadDatabase(database)
|
mEntryViewModel.loadDatabase(database)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,11 +479,12 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
mMainEntryId?.let { entryId ->
|
mMainEntryId?.let { entryId ->
|
||||||
EntryEditActivity.launchToUpdate(
|
EntryEditActivity.launch(
|
||||||
this,
|
activity = this,
|
||||||
database,
|
database = database,
|
||||||
entryId,
|
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||||
mEntryActivityResultLauncher
|
nodeId = entryId,
|
||||||
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,7 +522,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||||
setResult(Activity.RESULT_OK, this)
|
setResult(RESULT_OK, this)
|
||||||
}
|
}
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
@@ -527,34 +536,22 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open standard Entry activity
|
* Open standard or history Entry activity
|
||||||
*/
|
*/
|
||||||
fun launch(activity: Activity,
|
fun launch(
|
||||||
database: ContextualDatabase,
|
activity: Activity,
|
||||||
entryId: NodeId<UUID>,
|
database: ContextualDatabase,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
entryId: NodeId<UUID>,
|
||||||
|
historyPosition: Int? = null,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
activityResultLauncher.launch(intent)
|
historyPosition?.let {
|
||||||
}
|
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open history Entry activity
|
|
||||||
*/
|
|
||||||
fun launch(activity: Activity,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
historyPosition: Int,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
|
||||||
if (database.loaded) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
|
||||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
|
||||||
activityResultLauncher.launch(intent)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ import android.widget.Spinner
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.datepicker.MaterialDatePicker
|
import com.google.android.material.datepicker.MaterialDatePicker
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.timepicker.MaterialTimePicker
|
import com.google.android.material.timepicker.MaterialTimePicker
|
||||||
@@ -59,10 +59,10 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
|||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
@@ -101,6 +101,8 @@ import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
|||||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.EnumSet
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class EntryEditActivity : DatabaseLockActivity(),
|
class EntryEditActivity : DatabaseLockActivity(),
|
||||||
@@ -155,8 +157,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// To ask data lost only one time
|
override fun manageDatabaseInfo(): Boolean = true
|
||||||
private var backPressedAlreadyApproved = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -181,8 +182,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// To apply fit window with transparency
|
// To apply fit window with transparency
|
||||||
setTransparentNavigationBar(applyToStatusBar = true) {
|
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||||
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
|
container?.applyWindowInsets(EnumSet.of(
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
|
WindowInsetPosition.TOP_MARGINS,
|
||||||
|
WindowInsetPosition.BOTTOM_MARGINS,
|
||||||
|
WindowInsetPosition.START_MARGINS,
|
||||||
|
WindowInsetPosition.END_MARGINS,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||||
@@ -206,8 +211,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
mDatabase,
|
mDatabase,
|
||||||
entryId,
|
entryId,
|
||||||
parentId,
|
parentId,
|
||||||
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
|
intent.retrieveRegisterInfo()
|
||||||
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
?: intent.retrieveSearchInfo()?.toRegisterInfo()
|
||||||
)
|
)
|
||||||
|
|
||||||
// To retrieve attachment
|
// To retrieve attachment
|
||||||
@@ -374,30 +379,30 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
} ?: run {
|
} ?: run {
|
||||||
updateEntry(entrySave.oldEntry, entrySave.newEntry)
|
updateEntry(entrySave.oldEntry, entrySave.newEntry)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Don't wait for saving if it's to provide autofill
|
lifecycleScope.launch {
|
||||||
mDatabase?.let { database ->
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
EntrySelectionHelper.doSpecialAction(
|
mEntryEditViewModel.uiState.collect { uiState ->
|
||||||
intent = intent,
|
when (uiState) {
|
||||||
defaultAction = {},
|
EntryEditViewModel.UIState.Loading -> {}
|
||||||
searchAction = {},
|
EntryEditViewModel.UIState.ShowOverwriteMessage -> {
|
||||||
saveAction = {},
|
if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
|
||||||
keyboardSelectionAction = {
|
AlertDialog.Builder(this@EntryEditActivity)
|
||||||
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
.setTitle(R.string.warning_overwrite_data_title)
|
||||||
},
|
.setMessage(R.string.warning_overwrite_data_description)
|
||||||
autofillSelectionAction = { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||||
},
|
onCancelSpecialMode()
|
||||||
autofillRegistrationAction = {
|
}
|
||||||
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
},
|
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
|
||||||
passkeySelectionAction = {
|
}
|
||||||
entryValidatedForPasskeySelection(database, entrySave.newEntry)
|
.create().show()
|
||||||
},
|
}
|
||||||
passkeyRegistrationAction = {
|
}
|
||||||
entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,13 +415,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
mAllowCustomFields = database.allowEntryCustomFields() == true
|
||||||
mAllowOTP = database?.allowOTP == true
|
mAllowOTP = database.allowOTP == true
|
||||||
mEntryEditViewModel.loadDatabase(database)
|
mEntryEditViewModel.loadTemplateEntry(database)
|
||||||
mTemplatesSelectorAdapter?.apply {
|
mTemplatesSelectorAdapter?.apply {
|
||||||
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
iconDrawableFactory = database.iconDrawableFactory
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,6 +432,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
mEntryEditViewModel.unlockAction()
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
@@ -442,23 +448,27 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
searchAction = {
|
searchAction = {
|
||||||
// Nothing when search retrieved
|
// Nothing when search retrieved
|
||||||
},
|
},
|
||||||
saveAction = {
|
selectionAction = { intentSender, typeMode, searchInfo ->
|
||||||
entryValidatedForSave(entry)
|
when(typeMode) {
|
||||||
|
TypeMode.DEFAULT -> {}
|
||||||
|
TypeMode.MAGIKEYBOARD ->
|
||||||
|
entryValidatedForKeyboardSelection(database, entry)
|
||||||
|
TypeMode.PASSKEY ->
|
||||||
|
entryValidatedForPasskey(database, entry)
|
||||||
|
TypeMode.AUTOFILL ->
|
||||||
|
entryValidatedForAutofill(database, entry)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
keyboardSelectionAction = {
|
registrationAction = { _, typeMode, _ ->
|
||||||
entryValidatedForKeyboardSelection(database, entry)
|
when(typeMode) {
|
||||||
},
|
TypeMode.DEFAULT ->
|
||||||
autofillSelectionAction = { _, _ ->
|
entryValidatedForSave(entry)
|
||||||
entryValidatedForAutofillSelection(database, entry)
|
TypeMode.MAGIKEYBOARD -> {}
|
||||||
},
|
TypeMode.PASSKEY ->
|
||||||
autofillRegistrationAction = {
|
entryValidatedForPasskey(database, entry)
|
||||||
entryValidatedForAutofillRegistration(entry)
|
TypeMode.AUTOFILL ->
|
||||||
},
|
entryValidatedForAutofill(database, entry)
|
||||||
passkeySelectionAction = {
|
}
|
||||||
entryValidatedForPasskeySelection(database, entry)
|
|
||||||
},
|
|
||||||
passkeyRegistrationAction = {
|
|
||||||
entryValidatedForPasskeyRegistration(database, entry)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -477,46 +487,26 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
|
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
|
||||||
// Populate Magikeyboard with entry
|
// Build Magikeyboard response with the entry selected
|
||||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
this.buildSpecialModeResponseAndSetResult(
|
||||||
this,
|
entryInfo = entry.getEntryInfo(database),
|
||||||
entry.getEntryInfo(database)
|
extras = buildEntryResult(entry)
|
||||||
)
|
)
|
||||||
onValidateSpecialMode()
|
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
|
// Build Autofill response with the entry selected
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
|
this.buildSpecialModeResponseAndSetResult(
|
||||||
database,
|
entryInfo = entry.getEntryInfo(database),
|
||||||
entry.getEntryInfo(database))
|
extras = buildEntryResult(entry)
|
||||||
}
|
|
||||||
onValidateSpecialMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
this.buildPasskeyResponseAndSetResult(
|
|
||||||
entryInfo = entry.getEntryInfo(database)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
private fun entryValidatedForPasskey(database: ContextualDatabase, entry: Entry) {
|
||||||
//if (isIntentSender()) {
|
|
||||||
// TODO Autofill Callback #765
|
|
||||||
//}
|
|
||||||
onValidateSpecialMode()
|
|
||||||
if (!isIntentSender()) {
|
|
||||||
finishForEntryResult(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
this.buildPasskeyResponseAndSetResult(
|
this.buildPasskeyResponseAndSetResult(
|
||||||
entryInfo = entry.getEntryInfo(database),
|
entryInfo = entry.getEntryInfo(database),
|
||||||
@@ -757,13 +747,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onApprovedBackPressed(approved: () -> Unit) {
|
private fun onApprovedBackPressed(approved: () -> Unit) {
|
||||||
if (!backPressedAlreadyApproved) {
|
if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setMessage(R.string.discard_changes)
|
.setMessage(R.string.discard_changes)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.discard) { _, _ ->
|
.setPositiveButton(R.string.discard) { _, _ ->
|
||||||
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||||
backPressedAlreadyApproved = true
|
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||||
approved.invoke()
|
approved.invoke()
|
||||||
}.create().show()
|
}.create().show()
|
||||||
} else {
|
} else {
|
||||||
@@ -783,7 +773,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
val bundle = buildEntryResult(entry)
|
val bundle = buildEntryResult(entry)
|
||||||
val intentEntry = Intent()
|
val intentEntry = Intent()
|
||||||
intentEntry.putExtras(bundle)
|
intentEntry.putExtras(bundle)
|
||||||
setResult(Activity.RESULT_OK, intentEntry)
|
setResult(RESULT_OK, intentEntry)
|
||||||
super.finish()
|
super.finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Exception when parcelable can't be done
|
// Exception when parcelable can't be done
|
||||||
@@ -791,6 +781,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class RegistrationType {
|
||||||
|
UPDATE, CREATE
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = EntryEditActivity::class.java.name
|
private val TAG = EntryEditActivity::class.java.name
|
||||||
@@ -800,23 +794,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
const val KEY_PARENT = "parent"
|
const val KEY_PARENT = "parent"
|
||||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||||
|
|
||||||
fun registerForEntryResult(fragment: Fragment,
|
fun registerForEntryResult(
|
||||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
activity: FragmentActivity,
|
||||||
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
): ActivityResultLauncher<Intent> {
|
||||||
entryAddedOrUpdatedListener.invoke(
|
|
||||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
entryAddedOrUpdatedListener.invoke(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerForEntryResult(activity: FragmentActivity,
|
|
||||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
|
||||||
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == RESULT_OK) {
|
||||||
entryAddedOrUpdatedListener.invoke(
|
entryAddedOrUpdatedListener.invoke(
|
||||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||||
)
|
)
|
||||||
@@ -827,176 +810,72 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
* Launch EntryEditActivity to update an existing entry or to add a new entry in an existing group
|
||||||
*/
|
*/
|
||||||
fun launchToUpdate(activity: Activity,
|
fun launch(
|
||||||
database: ContextualDatabase,
|
activity: Activity,
|
||||||
entryId: NodeId<UUID>,
|
database: ContextualDatabase,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
registrationType: RegistrationType,
|
||||||
|
nodeId: NodeId<*>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
when (registrationType) {
|
||||||
|
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||||
|
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||||
|
}
|
||||||
activityResultLauncher.launch(intent)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to add a new entry in an existent group
|
* Launch EntryEditActivity to add a new entry in special selection
|
||||||
*/
|
*/
|
||||||
fun launchToCreate(activity: Activity,
|
fun launchForSelection(
|
||||||
database: ContextualDatabase,
|
context: Context,
|
||||||
groupId: NodeId<*>,
|
database: ContextualDatabase,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
typeMode: TypeMode,
|
||||||
if (database.loaded && !database.isReadOnly) {
|
groupId: NodeId<*>,
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
searchInfo: SearchInfo? = null,
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
) {
|
||||||
activityResultLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchToUpdateForSave(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
|
||||||
context,
|
|
||||||
intent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchToCreateForSave(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||||
context,
|
context = context,
|
||||||
intent,
|
intent = intent,
|
||||||
searchInfo
|
typeMode = typeMode,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
activityResultLauncher = activityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to add a new entry in keyboard selection
|
* Launch EntryEditActivity to update an updated entry or register a new entry (from autofill)
|
||||||
*/
|
*/
|
||||||
fun launchForKeyboardSelectionResult(context: Context,
|
fun launchForRegistration(
|
||||||
database: ContextualDatabase,
|
context: Context,
|
||||||
groupId: NodeId<*>,
|
database: ContextualDatabase,
|
||||||
searchInfo: SearchInfo? = null) {
|
nodeId: NodeId<*>,
|
||||||
|
registerInfo: RegisterInfo? = null,
|
||||||
|
typeMode: TypeMode,
|
||||||
|
registrationType: RegistrationType,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
|
) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
when (registrationType) {
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||||
context,
|
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||||
intent,
|
}
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to add a new entry in autofill selection
|
|
||||||
*/
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
|
||||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
activityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to add a new passkey entry
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
fun launchForPasskeySelectionResult(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
|
||||||
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
|
||||||
context,
|
|
||||||
intent,
|
|
||||||
activityResultLauncher,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to register an updated entry (from autofill)
|
|
||||||
*/
|
|
||||||
fun launchToUpdateForRegistration(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
registerInfo: RegisterInfo?,
|
|
||||||
typeMode: TypeMode) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
|
||||||
context,
|
|
||||||
activityResultLauncher,
|
|
||||||
intent,
|
|
||||||
registerInfo,
|
|
||||||
typeMode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to register a new entry (from autofill)
|
|
||||||
*/
|
|
||||||
fun launchToCreateForRegistration(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
registerInfo: RegisterInfo? = null,
|
|
||||||
typeMode: TypeMode) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -33,8 +32,6 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@@ -50,10 +47,8 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
|||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
@@ -99,8 +94,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
this.buildActivityResultLauncher()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -131,7 +125,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
uri?.let {
|
uri?.let {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchMainCredentialActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||||
@@ -160,7 +154,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||||
launchPasswordActivity(
|
launchMainCredentialActivity(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
||||||
fileDatabaseHistoryEntityToOpen.hardwareKey
|
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||||
@@ -179,7 +173,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
// Load default database the first time
|
// Load default database the first time
|
||||||
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
|
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
|
||||||
launchPasswordActivityWithPath(databaseFileUri)
|
launchMainCredentialActivityWithPath(databaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the database URI provided by file manager after an orientation change
|
// Retrieve the database URI provided by file manager after an orientation change
|
||||||
@@ -224,11 +218,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
if (database != null) {
|
|
||||||
launchGroupActivityIfLoaded(database)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
@@ -236,8 +227,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
// Update list
|
// Update list
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
@@ -263,13 +252,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
database,
|
database,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -287,17 +276,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||||
MainCredentialActivity.launch(this,
|
try {
|
||||||
databaseUri,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
keyFile,
|
intent = this.intent,
|
||||||
hardwareKey,
|
defaultAction = {
|
||||||
{ exception ->
|
MainCredentialActivity.launch(
|
||||||
fileNoFoundAction(exception)
|
activity = this,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{ onCancelSpecialMode() },
|
searchAction = { searchInfo ->
|
||||||
{ onLaunchActivitySpecialMode() },
|
MainCredentialActivity.launchForSearchResult(
|
||||||
mCredentialActivityResultLauncher)
|
activity = this,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
selectionAction = { intentSenderMode, typeMode, searchInfo ->
|
||||||
|
MainCredentialActivity.launchForSelection(
|
||||||
|
activity = this,
|
||||||
|
activityResultLauncher = if (intentSenderMode)
|
||||||
|
mCredentialActivityResultLauncher else null,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = typeMode,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
registrationAction = { intentSenderMode, typeMode, registerInfo ->
|
||||||
|
MainCredentialActivity.launchForRegistration(
|
||||||
|
activity = this,
|
||||||
|
activityResultLauncher = if (intentSenderMode)
|
||||||
|
mCredentialActivityResultLauncher else null,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = typeMode,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
@@ -307,12 +337,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mCredentialActivityResultLauncher)
|
mCredentialActivityResultLauncher
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
|
||||||
launchPasswordActivity(databaseUri, null, null)
|
launchMainCredentialActivity(databaseUri, null, null)
|
||||||
// Delete flickering for kitkat <=
|
// Delete flickering for kitkat <=
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
@@ -336,10 +367,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
launchGroupActivityIfLoaded(database)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show recent files if allowed
|
// Show recent files if allowed
|
||||||
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||||
databaseFilesViewModel.loadListOfDatabases()
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
@@ -358,7 +385,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
try {
|
try {
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
// Create the new database
|
// Create the new database
|
||||||
createDatabase(databaseUri, mainCredential)
|
mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val error = getString(R.string.error_create_database_file)
|
val error = getString(R.string.error_create_database_file)
|
||||||
@@ -442,71 +469,35 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForSearchResult(context: Context,
|
fun launchForSearch(
|
||||||
searchInfo: SearchInfo) {
|
context: Context,
|
||||||
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
searchInfo: SearchInfo
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
) {
|
||||||
searchInfo)
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
|
context = context,
|
||||||
|
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Save Launch
|
* Selection Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForSaveResult(context: Context,
|
fun launchForSelection(
|
||||||
searchInfo: SearchInfo) {
|
context: Context,
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
typeMode: TypeMode,
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
searchInfo: SearchInfo? = null,
|
||||||
searchInfo)
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
}
|
) {
|
||||||
|
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||||
/*
|
context = context,
|
||||||
* -------------------------
|
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
* Keyboard Launch
|
searchInfo = searchInfo,
|
||||||
* -------------------------
|
typeMode = typeMode,
|
||||||
*/
|
activityResultLauncher = activityResultLauncher
|
||||||
|
|
||||||
fun launchForKeyboardSelectionResult(activity: Activity,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Autofill Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
|
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
|
||||||
activityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Passkey Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
fun launchForPasskeySelectionResult(activity: Activity,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
|
||||||
activity,
|
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
|
||||||
activityResultLauncher,
|
|
||||||
searchInfo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,16 +506,18 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
context: Context,
|
||||||
registerInfo: RegisterInfo? = null,
|
typeMode: TypeMode,
|
||||||
typeMode: TypeMode) {
|
registerInfo: RegisterInfo? = null,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
) {
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context = context,
|
||||||
activityResultLauncher,
|
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
registerInfo = registerInfo,
|
||||||
registerInfo,
|
typeMode = 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
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -174,10 +176,10 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
if (database?.allowCustomIcons == true) {
|
if (database.allowCustomIcons) {
|
||||||
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
} else {
|
} else {
|
||||||
uploadButton.visibility = View.GONE
|
uploadButton.visibility = View.GONE
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
private lateinit var imageView: ImageView
|
private lateinit var imageView: ImageView
|
||||||
private lateinit var progressView: View
|
private lateinit var progressView: View
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -101,7 +103,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -119,18 +121,16 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
resources.displayMetrics.heightPixels * 2
|
resources.displayMetrics.heightPixels * 2
|
||||||
)
|
)
|
||||||
|
|
||||||
database?.let { database ->
|
BinaryDatabaseManager.loadBitmap(
|
||||||
BinaryDatabaseManager.loadBitmap(
|
database,
|
||||||
database,
|
attachment.binaryData,
|
||||||
attachment.binaryData,
|
mImagePreviewMaxWidth
|
||||||
mImagePreviewMaxWidth
|
) { bitmapLoaded ->
|
||||||
) { bitmapLoaded ->
|
if (bitmapLoaded == null) {
|
||||||
if (bitmapLoaded == null) {
|
finish()
|
||||||
finish()
|
} else {
|
||||||
} else {
|
progressView.visibility = View.GONE
|
||||||
progressView.visibility = View.GONE
|
imageView.setImageBitmap(bitmapLoaded)
|
||||||
imageView.setImageBitmap(bitmapLoaded)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: finish()
|
} ?: finish()
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
|
|||||||
private lateinit var validationButton: View
|
private lateinit var validationButton: View
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = true
|
||||||
|
|
||||||
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
|
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
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.DeviceUnlockManager
|
||||||
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
@@ -128,8 +124,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
private var mReadOnly: Boolean = false
|
private var mReadOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
|
|
||||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
this.buildActivityResultLauncher()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -310,26 +305,20 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
mDatabaseFileUri?.let { databaseFileUri ->
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
launchGroupActivityIfLoaded(database)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
if (database != null) {
|
// Trying to load another database
|
||||||
// Trying to load another database
|
if (mDatabaseFileUri != null
|
||||||
if (mDatabaseFileUri != null
|
&& database.fileUri != null
|
||||||
&& database.fileUri != null
|
&& mDatabaseFileUri != database.fileUri) {
|
||||||
&& mDatabaseFileUri != database.fileUri) {
|
Toast.makeText(this,
|
||||||
Toast.makeText(this,
|
R.string.warning_database_already_opened,
|
||||||
R.string.warning_database_already_opened,
|
Toast.LENGTH_LONG
|
||||||
Toast.LENGTH_LONG
|
).show()
|
||||||
).show()
|
|
||||||
}
|
|
||||||
launchGroupActivityIfLoaded(database)
|
|
||||||
}
|
}
|
||||||
|
launchGroupActivityIfLoaded(database)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
@@ -514,10 +503,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||||
// Consume the intent extra password
|
// Consume the intent extra password
|
||||||
intent.removeExtra(KEY_PASSWORD)
|
intent.removeExtra(KEY_PASSWORD)
|
||||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
mainCredentialView?.populatePasswordTextView(password)
|
mainCredentialView?.populatePasswordTextView(password)
|
||||||
}
|
}
|
||||||
|
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||||
|
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
|
||||||
if (launchImmediately) {
|
if (launchImmediately) {
|
||||||
loadDatabase()
|
loadDatabase()
|
||||||
} else {
|
} else {
|
||||||
@@ -572,10 +562,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mReadOnly && (
|
if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
|
||||||
mSpecialMode == SpecialMode.SAVE
|
|
||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|
||||||
) {
|
|
||||||
Log.e(TAG, getString(R.string.error_save_read_only))
|
Log.e(TAG, getString(R.string.error_save_read_only))
|
||||||
Snackbar.make(coordinatorLayout,
|
Snackbar.make(coordinatorLayout,
|
||||||
R.string.error_save_read_only,
|
R.string.error_save_read_only,
|
||||||
@@ -599,7 +586,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
loadDatabase(
|
mDatabaseViewModel.loadDatabase(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
mainCredential,
|
mainCredential,
|
||||||
readOnly,
|
readOnly,
|
||||||
@@ -752,11 +739,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
private const val KEY_PASSWORD = "password"
|
private const val KEY_PASSWORD = "password"
|
||||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
|
|
||||||
private fun buildAndLaunchIntent(activity: Activity,
|
private fun buildAndLaunchIntent(
|
||||||
databaseFile: Uri,
|
activity: Activity,
|
||||||
keyFile: Uri?,
|
databaseFile: Uri,
|
||||||
hardwareKey: HardwareKey?,
|
keyFile: Uri?,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
hardwareKey: HardwareKey?,
|
||||||
|
intentBuildLauncher: (Intent) -> Unit
|
||||||
|
) {
|
||||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||||
if (keyFile != null)
|
if (keyFile != null)
|
||||||
@@ -773,10 +762,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launch(activity: Activity,
|
fun launch(
|
||||||
databaseFile: Uri,
|
activity: Activity,
|
||||||
keyFile: Uri?,
|
databaseFile: Uri,
|
||||||
hardwareKey: HardwareKey?) {
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
@@ -789,245 +780,73 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForSearchResult(activity: Activity,
|
fun launchForSearchResult(
|
||||||
databaseFile: Uri,
|
activity: Activity,
|
||||||
keyFile: Uri?,
|
databaseFile: Uri,
|
||||||
hardwareKey: HardwareKey?,
|
keyFile: Uri?,
|
||||||
searchInfo: SearchInfo) {
|
hardwareKey: HardwareKey?,
|
||||||
|
searchInfo: SearchInfo
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
activity,
|
context = activity,
|
||||||
intent,
|
intent = intent,
|
||||||
searchInfo)
|
searchInfo = searchInfo
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Save Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForSaveResult(activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Keyboard Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForKeyboardResult(activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Autofill Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
activityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Passkey Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForPasskeyResult(activity: Activity,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
activityResultLauncher,
|
|
||||||
searchInfo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Selection Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForSelection(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
typeMode: TypeMode,
|
typeMode: TypeMode,
|
||||||
registerInfo: RegisterInfo?
|
searchInfo: SearchInfo?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||||
) {
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||||
context = activity,
|
context = activity,
|
||||||
activityResultLauncher = activityResultLauncher,
|
|
||||||
intent = intent,
|
intent = intent,
|
||||||
typeMode = typeMode,
|
typeMode = typeMode,
|
||||||
registerInfo = registerInfo
|
searchInfo = searchInfo,
|
||||||
|
activityResultLauncher = activityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Global Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launch(activity: AppCompatActivity,
|
|
||||||
databaseUri: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
|
||||||
onCancelSpecialMode: () -> Unit,
|
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?) {
|
|
||||||
|
|
||||||
try {
|
@Throws(FileNotFoundException::class)
|
||||||
EntrySelectionHelper.doSpecialAction(
|
fun launchForRegistration(
|
||||||
intent = activity.intent,
|
activity: Activity,
|
||||||
defaultAction = {
|
databaseFile: Uri,
|
||||||
launch(
|
keyFile: Uri?,
|
||||||
activity = activity,
|
hardwareKey: HardwareKey?,
|
||||||
databaseFile = databaseUri,
|
typeMode: TypeMode,
|
||||||
keyFile = keyFile,
|
registerInfo: RegisterInfo?,
|
||||||
hardwareKey = hardwareKey
|
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||||
)
|
) {
|
||||||
},
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
searchAction = { searchInfo ->
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
launchForSearchResult(
|
context = activity,
|
||||||
activity = activity,
|
intent = intent,
|
||||||
databaseFile = databaseUri,
|
typeMode = typeMode,
|
||||||
keyFile = keyFile,
|
registerInfo = registerInfo,
|
||||||
hardwareKey = hardwareKey,
|
activityResultLauncher = activityResultLauncher,
|
||||||
searchInfo = searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
saveAction = { searchInfo ->
|
|
||||||
launchForSaveResult(
|
|
||||||
activity = activity,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
searchInfo = searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
keyboardSelectionAction = { searchInfo ->
|
|
||||||
launchForKeyboardResult(
|
|
||||||
activity = activity,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
searchInfo = searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
autofillSelectionAction = { searchInfo, autofillComponent ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
launchForAutofillResult(
|
|
||||||
activity = activity,
|
|
||||||
activityResultLauncher = activityResultLauncher,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
autofillComponent = autofillComponent,
|
|
||||||
searchInfo = searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
} else {
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
autofillRegistrationAction = { registerInfo ->
|
|
||||||
launchForRegistration(
|
|
||||||
activity = activity,
|
|
||||||
activityResultLauncher = activityResultLauncher,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
typeMode = TypeMode.AUTOFILL,
|
|
||||||
registerInfo = registerInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
passkeySelectionAction = { searchInfo ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
launchForPasskeyResult(
|
|
||||||
activity = activity,
|
|
||||||
activityResultLauncher = activityResultLauncher,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
searchInfo = searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
} else {
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
passkeyRegistrationAction = { registerInfo ->
|
|
||||||
launchForRegistration(
|
|
||||||
activity = activity,
|
|
||||||
activityResultLauncher = activityResultLauncher,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
typeMode = TypeMode.PASSKEY,
|
|
||||||
registerInfo = registerInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
builder.setMessage(stringBuilder)
|
builder.setMessage(stringBuilder)
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
actionDatabaseListener?.validateDatabaseChanged()
|
actionDatabaseListener?.onDatabaseChangeValidated()
|
||||||
}
|
}
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ActionDatabaseChangedListener {
|
interface ActionDatabaseChangedListener {
|
||||||
fun validateDatabaseChanged()
|
fun onDatabaseChangeValidated()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -86,9 +86,10 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||||
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
|
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
|
||||||
|
|
||||||
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
fun getInstance(
|
||||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
readOnly: Boolean
|
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
readOnly: Boolean
|
||||||
)
|
)
|
||||||
: DatabaseChangedDialogFragment {
|
: DatabaseChangedDialogFragment {
|
||||||
val fragment = DatabaseChangedDialogFragment()
|
val fragment = DatabaseChangedDialogFragment()
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import android.view.View
|
|||||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -12,23 +15,40 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
private var mDatabase: ContextualDatabase? = null
|
private val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch {
|
||||||
mDatabaseViewModel.database.observe(this) { database ->
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
this.mDatabase = database
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
resetAppTimeoutOnTouchOrFocus()
|
when (uiState) {
|
||||||
onDatabaseRetrieved(database)
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
database?.let {
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +72,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
|||||||
resetAppTimeoutOnTouchOrFocus()
|
resetAppTimeoutOnTouchOrFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Can be overridden by a subclass
|
// Can be overridden by a subclass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
|||||||
private lateinit var uuidContainerView: ViewGroup
|
private lateinit var uuidContainerView: ViewGroup
|
||||||
private lateinit var uuidReferenceView: TextView
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
}
|
}
|
||||||
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||||
|
|
||||||
if (database?.allowCustomSearchableGroup() == true) {
|
if (database.allowCustomSearchableGroup()) {
|
||||||
searchableLabelView.visibility = View.VISIBLE
|
searchableLabelView.visibility = View.VISIBLE
|
||||||
searchableView.visibility = View.VISIBLE
|
searchableView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -112,32 +112,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
}
|
}
|
||||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
|
|
||||||
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
|
searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (database?.allowAutoType() == true) {
|
if (database.allowAutoType()) {
|
||||||
autoTypeContainerView.visibility = View.VISIBLE
|
autoTypeContainerView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
autoTypeContainerView.visibility = View.GONE
|
autoTypeContainerView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||||
tagsCompletionView.apply {
|
tagsCompletionView.apply {
|
||||||
threshold = 1
|
threshold = 1
|
||||||
setAdapter(tagsAdapter)
|
setAdapter(tagsAdapter)
|
||||||
}
|
}
|
||||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
private var mCustomIcon: IconImageCustom? = null
|
private var mCustomIcon: IconImageCustom? = null
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
|
||||||
}
|
}
|
||||||
mCustomIcon?.let { customIcon ->
|
mCustomIcon?.let { customIcon ->
|
||||||
populateViewsWithCustomIcon(customIcon)
|
populateViewsWithCustomIcon(customIcon)
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ import com.google.android.material.textfield.TextInputLayout
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
|
||||||
import com.kunzisoft.keepass.password.PasswordEntropy
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
@@ -258,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
showEmptyPasswordConfirmationDialog()
|
showEmptyPasswordConfirmationDialog()
|
||||||
} else if (!error
|
} else if (!error
|
||||||
&& hardwareKey != null
|
&& hardwareKey != null
|
||||||
&& !HardwareKeyActivity.isHardwareKeyAvailable(
|
&& !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
|
||||||
requireActivity(), hardwareKey, false)
|
|
||||||
) {
|
) {
|
||||||
// show hardware driver dialog if required
|
// show hardware driver dialog if required
|
||||||
error = true
|
error = true
|
||||||
|
|||||||
@@ -4,36 +4,59 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
protected var mDatabase: ContextualDatabase? = null
|
protected val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
if (mDatabase == null || mDatabase != database) {
|
super.onCreate(savedInstanceState)
|
||||||
this.mDatabase = database
|
lifecycleScope.launch {
|
||||||
onDatabaseRetrieved(database)
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
database?.let {
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
||||||
context?.let {
|
context?.let {
|
||||||
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
|
view?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||||
|
context = it,
|
||||||
|
databaseLoaded = mDatabase?.loaded
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +67,4 @@ abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
|||||||
) {
|
) {
|
||||||
// Can be overridden by a subclass
|
// Can be overridden by a subclass
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun buildNewBinaryAttachment(): BinaryData? {
|
|
||||||
return mDatabase?.buildNewBinaryAttachment()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
val attachmentToUploadUri = it.attachmentToUploadUri
|
val attachmentToUploadUri = it.attachmentToUploadUri
|
||||||
val fileName = it.fileName
|
val fileName = it.fileName
|
||||||
|
|
||||||
buildNewBinaryAttachment()?.let { binaryAttachment ->
|
mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
|
||||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
// Ask to replace the current attachment
|
// Ask to replace the current attachment
|
||||||
if ((!mAllowMultipleAttachments
|
if ((!mAllowMultipleAttachments
|
||||||
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
|
|
||||||
templateView.populateIconMethod = { imageView, icon ->
|
templateView.populateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
|
mAllowMultipleAttachments = database.allowMultipleAttachments == true
|
||||||
|
|
||||||
attachmentsAdapter?.database = database
|
attachmentsAdapter?.database = database
|
||||||
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||||
tagsCompletionView.apply {
|
tagsCompletionView.apply {
|
||||||
threshold = 1
|
threshold = 1
|
||||||
setAdapter(tagsAdapter)
|
setAdapter(tagsAdapter)
|
||||||
}
|
}
|
||||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||||
attachmentsAdapter?.database = database
|
attachmentsAdapter?.database = database
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
@@ -154,46 +154,44 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
database?.let { database ->
|
mAdapter = NodesAdapter(context, database).apply {
|
||||||
mAdapter = NodesAdapter(context, database).apply {
|
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
||||||
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
if (nodeActionSelectionMode) {
|
||||||
if (nodeActionSelectionMode) {
|
if (listActionNodes.contains(node)) {
|
||||||
if (listActionNodes.contains(node)) {
|
// Remove selected item if already selected
|
||||||
// Remove selected item if already selected
|
listActionNodes.remove(node)
|
||||||
listActionNodes.remove(node)
|
|
||||||
} else {
|
|
||||||
// Add selected item if not already selected
|
|
||||||
listActionNodes.add(node)
|
|
||||||
}
|
|
||||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
|
||||||
setActionNodes(listActionNodes)
|
|
||||||
notifyNodeChanged(node)
|
|
||||||
} else {
|
} else {
|
||||||
nodeClickListener?.onNodeClick(database, node)
|
// Add selected item if not already selected
|
||||||
|
listActionNodes.add(node)
|
||||||
}
|
}
|
||||||
|
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||||
|
setActionNodes(listActionNodes)
|
||||||
|
notifyNodeChanged(node)
|
||||||
|
} else {
|
||||||
|
nodeClickListener?.onNodeClick(database, node)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
|
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
|
||||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||||
// Select the first item after a long click
|
// Select the first item after a long click
|
||||||
if (!listActionNodes.contains(node))
|
if (!listActionNodes.contains(node))
|
||||||
listActionNodes.add(node)
|
listActionNodes.add(node)
|
||||||
|
|
||||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||||
|
|
||||||
setActionNodes(listActionNodes)
|
setActionNodes(listActionNodes)
|
||||||
notifyNodeChanged(node)
|
notifyNodeChanged(node)
|
||||||
activity?.hideKeyboard()
|
activity?.hideKeyboard()
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
})
|
return true
|
||||||
}
|
}
|
||||||
mNodesRecyclerView?.adapter = mAdapter
|
})
|
||||||
}
|
}
|
||||||
|
mNodesRecyclerView?.adapter = mAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +246,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
||||||
activity?.intent?.let {
|
activity?.intent?.let {
|
||||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
specialMode = it.retrieveSpecialMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,9 +297,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun containsRecycleBin(nodes: List<Node>): Boolean {
|
private fun containsRecycleBin(database: ContextualDatabase?, nodes: List<Node>): Boolean {
|
||||||
return mDatabase?.isRecycleBinEnabled == true
|
return database?.isRecycleBinEnabled == true
|
||||||
&& nodes.any { it == mDatabase?.recycleBin }
|
&& nodes.any { it == database.recycleBin }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(database: ContextualDatabase,
|
fun actionNodesCallback(database: ContextualDatabase,
|
||||||
@@ -328,7 +326,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
// Open and Edit for a single item
|
// Open and Edit for a single item
|
||||||
if (nodes.size == 1) {
|
if (nodes.size == 1) {
|
||||||
// Edition
|
// Edition
|
||||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -348,7 +346,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deletion
|
// Deletion
|
||||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||||
menu?.removeItem(R.id.menu_delete)
|
menu?.removeItem(R.id.menu_delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
|||||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val populateList = launch {
|
val populateList = launch {
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ class IconPickerFragment : DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||||
if (database?.allowCustomIcons == true) 2 else 1)
|
if (database.allowCustomIcons) 2 else 1)
|
||||||
viewPager.adapter = iconPickerPagerAdapter
|
viewPager.adapter = iconPickerPagerAdapter
|
||||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||||
tab.text = when (position) {
|
tab.text = when (position) {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -293,20 +293,22 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
private fun generatePassword() {
|
private fun generatePassword() {
|
||||||
var password = ""
|
var password = ""
|
||||||
try {
|
try {
|
||||||
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
|
password = PasswordGenerator(resources).generatePassword(
|
||||||
uppercaseCompound.isChecked,
|
length = getPasswordLength(),
|
||||||
lowercaseCompound.isChecked,
|
upperCase = uppercaseCompound.isChecked,
|
||||||
digitsCompound.isChecked,
|
lowerCase = lowercaseCompound.isChecked,
|
||||||
minusCompound.isChecked,
|
digits = digitsCompound.isChecked,
|
||||||
underlineCompound.isChecked,
|
minus = minusCompound.isChecked,
|
||||||
spaceCompound.isChecked,
|
underline = underlineCompound.isChecked,
|
||||||
specialsCompound.isChecked,
|
space = spaceCompound.isChecked,
|
||||||
bracketsCompound.isChecked,
|
specials = specialsCompound.isChecked,
|
||||||
extendedCompound.isChecked,
|
brackets = bracketsCompound.isChecked,
|
||||||
getConsiderChars(),
|
extended = extendedCompound.isChecked,
|
||||||
getIgnoreChars(),
|
considerChars = getConsiderChars(),
|
||||||
atLeastOneCompound.isChecked,
|
ignoreChars = getIgnoreChars(),
|
||||||
excludeAmbiguousCompound.isChecked)
|
atLeastOneFromEach = atLeastOneCompound.isChecked,
|
||||||
|
excludeAmbiguousChar = excludeAmbiguousCompound.isChecked
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to generate a password", e)
|
Log.e(TAG, "Unable to generate a password", e)
|
||||||
}
|
}
|
||||||
@@ -318,7 +320,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,236 @@
|
|||||||
package com.kunzisoft.keepass.activities.legacy
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
import android.net.Uri
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||||
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||||
|
import com.kunzisoft.keepass.tasks.ProgressTaskViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
|
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
|
||||||
|
|
||||||
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
protected val mDatabase: ContextualDatabase?
|
||||||
protected var mDatabase: ContextualDatabase? = null
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
|
private 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch {
|
||||||
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
when (uiState) {
|
||||||
val databaseWasReloaded = database?.wasReloaded == true
|
is DatabaseViewModel.ActionState.Loading -> {}
|
||||||
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
|
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
|
||||||
finish()
|
if (finishActivityIfReloadRequested()) {
|
||||||
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
|
finish()
|
||||||
database?.wasReloaded = false
|
}
|
||||||
onDatabaseRetrieved(database)
|
}
|
||||||
|
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.start(uiState.progressMessage)
|
||||||
|
}
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
|
||||||
|
progressTaskViewModel.update(uiState.progressMessage)
|
||||||
|
}
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
|
||||||
|
progressTaskViewModel.stop()
|
||||||
|
}
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
progressTaskViewModel.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
|
lifecycleScope.launch {
|
||||||
onDatabaseActionFinished(database, actionTask, result)
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
progressTaskViewModel.progressTaskState.collect { state ->
|
||||||
|
when (state) {
|
||||||
|
ProgressTaskViewModel.ProgressTaskState.Start ->
|
||||||
|
showDialog()
|
||||||
|
ProgressTaskViewModel.ProgressTaskState.Stop ->
|
||||||
|
stopDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
// Nullable function
|
||||||
|
onUnknownDatabaseRetrieved(database)
|
||||||
|
database?.let {
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun showDatabaseDialog(): Boolean {
|
/**
|
||||||
return true
|
* Nullable function to retrieve a database
|
||||||
}
|
*/
|
||||||
|
open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
mDatabaseTaskProvider?.destroy()
|
|
||||||
mDatabaseTaskProvider = null
|
|
||||||
mDatabase = null
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
mDatabase = database
|
|
||||||
mDatabaseViewModel.defineDatabase(database)
|
|
||||||
// optional method implementation
|
// optional method implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
|
// optional method implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun manageDatabaseInfo(): Boolean = true
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
mDatabaseViewModel.onActionFinished(database, actionTask, result)
|
|
||||||
// optional method implementation
|
// optional method implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDatabase(
|
private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
|
||||||
databaseUri: Uri,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
mainCredential: MainCredential
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
== PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
startDatabaseService(bundle, actionTask)
|
||||||
|
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// it's not the first time, so the user deliberately chooses not to display the notification
|
||||||
|
startDatabaseService(bundle, actionTask)
|
||||||
|
} else {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.warning_database_notification_permission)
|
||||||
|
.setNegativeButton(R.string.later) { _, _ ->
|
||||||
|
// Refuses the notification, so start the service
|
||||||
|
startDatabaseService(bundle, actionTask)
|
||||||
|
}
|
||||||
|
.setPositiveButton(R.string.ask) { _, _ ->
|
||||||
|
// Save the temp parameters to ask the permission
|
||||||
|
tempServiceParameters.add(Pair(bundle, actionTask))
|
||||||
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}.create().show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startDatabaseService(bundle, actionTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDatabaseChangedDialog(
|
||||||
|
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
readOnlyDatabase: Boolean
|
||||||
) {
|
) {
|
||||||
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
lifecycleScope.launch {
|
||||||
|
if (databaseChangedDialogFragment == null) {
|
||||||
|
databaseChangedDialogFragment = supportFragmentManager
|
||||||
|
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||||
|
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||||
|
mActionDatabaseListener
|
||||||
|
}
|
||||||
|
if (progressTaskDialogFragment == null) {
|
||||||
|
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||||
|
previousDatabaseInfo,
|
||||||
|
newDatabaseInfo,
|
||||||
|
readOnlyDatabase
|
||||||
|
)
|
||||||
|
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||||
|
mActionDatabaseListener
|
||||||
|
databaseChangedDialogFragment?.show(
|
||||||
|
supportFragmentManager,
|
||||||
|
DATABASE_CHANGED_DIALOG_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadDatabase(
|
private fun showDialog() {
|
||||||
databaseUri: Uri,
|
lifecycleScope.launch {
|
||||||
mainCredential: MainCredential,
|
if (showDatabaseDialog()) {
|
||||||
readOnly: Boolean,
|
if (progressTaskDialogFragment == null) {
|
||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
progressTaskDialogFragment = supportFragmentManager
|
||||||
fixDuplicateUuid: Boolean
|
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||||
) {
|
}
|
||||||
mDatabaseTaskProvider?.startDatabaseLoad(
|
if (progressTaskDialogFragment == null) {
|
||||||
databaseUri,
|
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||||
mainCredential,
|
progressTaskDialogFragment?.show(
|
||||||
readOnly,
|
supportFragmentManager,
|
||||||
cipherEncryptDatabase,
|
PROGRESS_TASK_DIALOG_TAG
|
||||||
fixDuplicateUuid
|
)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun closeDatabase() {
|
private fun stopDialog() {
|
||||||
mDatabase?.clearAndClose(this.getBinaryDir())
|
progressTaskDialogFragment?.dismissAllowingStateLoss()
|
||||||
|
progressTaskDialogFragment = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
protected open fun showDatabaseDialog(): Boolean {
|
||||||
super.onResume()
|
return true
|
||||||
mDatabaseTaskProvider?.registerProgressTask()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
|
||||||
super.onPause()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
@@ -87,128 +87,44 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
deleteDatabaseNodes(nodes)
|
deleteDatabaseNodes(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.saveDatabase.observe(this) { save ->
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
|
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge(save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
|
||||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveName.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveDescription.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveColor.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveCompression.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.removeUnlinkData.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveRecycleBin.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveEncryption.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveKeyDerivation.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveIterations.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveMemoryUsage.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveParallelism.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mExitLock = false
|
mExitLock = false
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
// End activity if database not loaded
|
// End activity if database not loaded
|
||||||
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
|
if (database.loaded.not())
|
||||||
finish()
|
finish()
|
||||||
}
|
|
||||||
|
|
||||||
// Focus view to reinitialize timeout,
|
// Focus view to reinitialize timeout,
|
||||||
// view is not necessary loaded so retry later in resume
|
// view is not necessary loaded so retry later in resume
|
||||||
viewToInvalidateTimeout()
|
viewToInvalidateTimeout()
|
||||||
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
|
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
|
||||||
|
|
||||||
database?.let {
|
// check timeout
|
||||||
// check timeout
|
if (mTimeoutEnable) {
|
||||||
if (mTimeoutEnable) {
|
if (mLockReceiver == null) {
|
||||||
if (mLockReceiver == null) {
|
mLockReceiver = LockReceiver {
|
||||||
mLockReceiver = LockReceiver {
|
closeDatabase(database)
|
||||||
mDatabase = null
|
mExitLock = true
|
||||||
closeDatabase(database)
|
closeOptionsMenu()
|
||||||
mExitLock = true
|
finish()
|
||||||
closeOptionsMenu()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
registerLockReceiver(mLockReceiver)
|
|
||||||
}
|
}
|
||||||
|
registerLockReceiver(mLockReceiver)
|
||||||
// After the first creation
|
|
||||||
// or If simply swipe with another application
|
|
||||||
// If the time is out -> close the Activity
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
|
||||||
// If onCreate already record time
|
|
||||||
if (!mExitLock)
|
|
||||||
TimeoutHelper.recordTime(this, database.loaded)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseReadOnly = database.isReadOnly
|
// After the first creation
|
||||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
// or If simply swipe with another application
|
||||||
|
// If the time is out -> close the Activity
|
||||||
checkRegister()
|
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||||
|
// If onCreate already record time
|
||||||
|
if (!mExitLock)
|
||||||
|
TimeoutHelper.recordTime(this, database.loaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mDatabaseReadOnly = database.isReadOnly
|
||||||
|
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||||
|
|
||||||
|
checkRegister()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
@@ -227,7 +143,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
@@ -249,24 +164,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
databaseUri: Uri?,
|
databaseUri: Uri?,
|
||||||
mainCredential: MainCredential
|
mainCredential: MainCredential
|
||||||
) {
|
) {
|
||||||
assignDatabasePassword(databaseUri, mainCredential)
|
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignDatabasePassword(
|
fun assignMainCredential(mainCredential: MainCredential) {
|
||||||
databaseUri: Uri?,
|
|
||||||
mainCredential: MainCredential
|
|
||||||
) {
|
|
||||||
if (databaseUri != null) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assignPassword(mainCredential: MainCredential) {
|
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
database.fileUri?.let { databaseUri ->
|
database.fileUri?.let { databaseUri ->
|
||||||
// Show the progress dialog now or after dialog confirmation
|
// Show the progress dialog now or after dialog confirmation
|
||||||
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
|
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
|
||||||
assignDatabasePassword(databaseUri, mainCredential)
|
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
|
||||||
} else {
|
} else {
|
||||||
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
||||||
.show(supportFragmentManager, "passwordEncodingTag")
|
.show(supportFragmentManager, "passwordEncodingTag")
|
||||||
@@ -276,45 +182,51 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveDatabase() {
|
fun saveDatabase() {
|
||||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
mDatabaseViewModel.saveDatabase(save = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDatabaseTo(uri: Uri) {
|
fun saveDatabaseTo(uri: Uri) {
|
||||||
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
|
mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeDatabase() {
|
fun mergeDatabase() {
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
|
mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
|
mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadDatabase() {
|
fun reloadDatabase() {
|
||||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
|
||||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createEntry(newEntry: Entry,
|
fun createEntry(
|
||||||
parent: Group) {
|
newEntry: Entry,
|
||||||
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
|
parent: Group
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateEntry(oldEntry: Entry,
|
fun updateEntry(
|
||||||
entryToUpdate: Entry) {
|
oldEntry: Entry,
|
||||||
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
entryToUpdate: Entry
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyNodes(nodesToCopy: List<Node>,
|
fun copyNodes(
|
||||||
newParent: Group) {
|
nodesToCopy: List<Node>,
|
||||||
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
newParent: Group
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveNodes(nodesToMove: List<Node>,
|
fun moveNodes(
|
||||||
newParent: Group) {
|
nodesToMove: List<Node>,
|
||||||
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
newParent: Group
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
|
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
|
||||||
@@ -330,6 +242,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
|
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
|
||||||
|
// TODO Move in ViewModel
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
// If recycle bin enabled, ensure it exists
|
// If recycle bin enabled, ensure it exists
|
||||||
if (database.isRecycleBinEnabled) {
|
if (database.isRecycleBinEnabled) {
|
||||||
@@ -350,11 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
||||||
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
|
mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createGroup(parent: Group,
|
fun createGroup(
|
||||||
groupInfo: GroupInfo?) {
|
parent: Group,
|
||||||
|
groupInfo: GroupInfo?
|
||||||
|
) {
|
||||||
|
// TODO Move in ViewModel
|
||||||
// Build the group
|
// Build the group
|
||||||
mDatabase?.createGroup()?.let { newGroup ->
|
mDatabase?.createGroup()?.let { newGroup ->
|
||||||
groupInfo?.let { info ->
|
groupInfo?.let { info ->
|
||||||
@@ -362,12 +278,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
// Not really needed here because added in runnable but safe
|
// Not really needed here because added in runnable but safe
|
||||||
newGroup.parent = parent
|
newGroup.parent = parent
|
||||||
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
|
mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateGroup(oldGroup: Group,
|
fun updateGroup(
|
||||||
groupInfo: GroupInfo) {
|
oldGroup: Group,
|
||||||
|
groupInfo: GroupInfo
|
||||||
|
) {
|
||||||
|
// TODO Move in ViewModel
|
||||||
// If group updated save it in the database
|
// If group updated save it in the database
|
||||||
val updateGroup = Group(oldGroup).let { updateGroup ->
|
val updateGroup = Group(oldGroup).let { updateGroup ->
|
||||||
updateGroup.apply {
|
updateGroup.apply {
|
||||||
@@ -377,27 +296,28 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
this.setGroupInfo(groupInfo)
|
this.setGroupInfo(groupInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
|
fun restoreEntryHistory(
|
||||||
entryHistoryPosition: Int) {
|
mainEntryId: NodeId<UUID>,
|
||||||
mDatabaseTaskProvider
|
entryHistoryPosition: Int
|
||||||
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
) {
|
||||||
|
mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
|
fun deleteEntryHistory(
|
||||||
entryHistoryPosition: Int) {
|
mainEntryId: NodeId<UUID>,
|
||||||
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
entryHistoryPosition: Int
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkRegister() {
|
private fun checkRegister() {
|
||||||
// If in ave or registration mode, don't allow read only
|
// If in registration mode, don't allow read only
|
||||||
if ((mSpecialMode == SpecialMode.SAVE
|
if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
|
||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|
||||||
&& mDatabaseReadOnly) {
|
|
||||||
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,9 +370,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.lock) { _, _ ->
|
.setPositiveButton(R.string.lock) { _, _ ->
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
finish()
|
||||||
}.create().show()
|
}.create().show()
|
||||||
} else {
|
} else {
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
package com.kunzisoft.keepass.activities.legacy
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
@@ -21,10 +30,25 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
|
|||||||
abstract class DatabaseModeActivity : DatabaseActivity() {
|
abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||||
|
|
||||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||||
|
|
||||||
private var mToolbarSpecial: ToolbarSpecial? = null
|
private var mToolbarSpecial: ToolbarSpecial? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility activity result launcher,
|
||||||
|
* Used recursively, close each activity with return data
|
||||||
|
*/
|
||||||
|
protected open var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = false,
|
||||||
|
resultCode = it.resultCode,
|
||||||
|
data = it.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
open fun onDatabaseBackPressed() {
|
open fun onDatabaseBackPressed() {
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT)
|
if (mSpecialMode != SpecialMode.DEFAULT)
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
@@ -50,8 +74,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
|
|
||||||
fun onLaunchActivitySpecialMode() {
|
fun onLaunchActivitySpecialMode() {
|
||||||
if (!isIntentSender()) {
|
if (!isIntentSender()) {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,8 +84,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
if (isIntentSender()) {
|
if (isIntentSender()) {
|
||||||
super.finish()
|
super.finish()
|
||||||
} else {
|
} else {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
backToTheMainAppAndFinish()
|
backToTheMainAppAndFinish()
|
||||||
}
|
}
|
||||||
@@ -73,8 +97,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
// To get the app caller, only for IntentSender
|
// To get the app caller, only for IntentSender
|
||||||
onRegularBackPressed()
|
onRegularBackPressed()
|
||||||
} else {
|
} else {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
backToTheMainAppAndFinish()
|
backToTheMainAppAndFinish()
|
||||||
}
|
}
|
||||||
@@ -105,18 +129,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
mSpecialMode = intent.retrieveSpecialMode()
|
||||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
mTypeMode = intent.retrieveTypeMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
mSpecialMode = intent.retrieveSpecialMode()
|
||||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
mTypeMode = intent.retrieveTypeMode()
|
||||||
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
|
||||||
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
||||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
?: intent.retrieveSearchInfo()
|
||||||
|
|
||||||
// To show the selection mode
|
// To show the selection mode
|
||||||
mToolbarSpecial = findViewById(R.id.special_mode_view)
|
mToolbarSpecial = findViewById(R.id.special_mode_view)
|
||||||
@@ -125,9 +149,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
val selectionModeStringId = when (mSpecialMode) {
|
val selectionModeStringId = when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT, // Not important because hidden
|
SpecialMode.DEFAULT, // Not important because hidden
|
||||||
SpecialMode.SEARCH -> R.string.search_mode
|
SpecialMode.SEARCH -> R.string.search_mode
|
||||||
SpecialMode.SAVE -> R.string.save_mode
|
|
||||||
SpecialMode.SELECTION -> R.string.selection_mode
|
SpecialMode.SELECTION -> R.string.selection_mode
|
||||||
SpecialMode.REGISTRATION -> R.string.registration_mode
|
SpecialMode.REGISTRATION -> R.string.save_mode // Save is registration mode
|
||||||
}
|
}
|
||||||
val typeModeStringId = when (mTypeMode) {
|
val typeModeStringId = when (mTypeMode) {
|
||||||
TypeMode.DEFAULT, // Not important because hidden
|
TypeMode.DEFAULT, // Not important because hidden
|
||||||
@@ -145,7 +168,6 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
visible = when (mSpecialMode) {
|
visible = when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> false
|
SpecialMode.DEFAULT -> false
|
||||||
SpecialMode.SEARCH -> true
|
SpecialMode.SEARCH -> true
|
||||||
SpecialMode.SAVE -> true
|
|
||||||
SpecialMode.SELECTION -> true
|
SpecialMode.SELECTION -> true
|
||||||
SpecialMode.REGISTRATION -> true
|
SpecialMode.REGISTRATION -> true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
|
||||||
interface DatabaseRetrieval {
|
interface DatabaseRetrieval {
|
||||||
fun onDatabaseRetrieved(database: ContextualDatabase?)
|
fun onDatabaseRetrieved(database: ContextualDatabase)
|
||||||
fun onDatabaseActionFinished(database: ContextualDatabase,
|
|
||||||
actionTask: String,
|
fun onDatabaseActionFinished(
|
||||||
result: ActionRunnable.Result)
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -24,26 +24,31 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.getEnum
|
||||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
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.putEnumExtra
|
||||||
|
import com.kunzisoft.keepass.utils.putParcelableList
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
object EntrySelectionHelper {
|
||||||
|
|
||||||
@@ -51,6 +56,8 @@ object EntrySelectionHelper {
|
|||||||
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
||||||
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||||
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||||
|
private const val EXTRA_NODES_IDS = "com.kunzisoft.keepass.extra.NODES_IDS"
|
||||||
|
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.NODE_ID"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finish the activity by passing the result code and by locking the database if necessary
|
* Finish the activity by passing the result code and by locking the database if necessary
|
||||||
@@ -58,7 +65,7 @@ object EntrySelectionHelper {
|
|||||||
fun Activity.setActivityResult(
|
fun Activity.setActivityResult(
|
||||||
lockDatabase: Boolean = false,
|
lockDatabase: Boolean = false,
|
||||||
resultCode: Int,
|
resultCode: Int,
|
||||||
data: Intent? = null,
|
data: Intent? = null
|
||||||
) {
|
) {
|
||||||
when (resultCode) {
|
when (resultCode) {
|
||||||
Activity.RESULT_OK ->
|
Activity.RESULT_OK ->
|
||||||
@@ -68,170 +75,212 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
this.finish()
|
this.finish()
|
||||||
|
|
||||||
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
if (lockDatabase) {
|
||||||
// Close the database
|
// Close the database
|
||||||
this.sendBroadcast(Intent(LOCK_ACTION))
|
this.sendBroadcast(Intent(LOCK_ACTION))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun startActivityForSearchModeResult(
|
||||||
* Utility method to build a registerForActivityResult,
|
context: Context,
|
||||||
* Used recursively, close each activity with return data
|
intent: Intent,
|
||||||
*/
|
searchInfo: SearchInfo
|
||||||
fun AppCompatActivity.buildActivityResultLauncher(
|
) {
|
||||||
lockDatabase: Boolean = false,
|
intent.addSpecialMode(SpecialMode.SEARCH)
|
||||||
dataTransformation: (data: Intent?) -> Intent? = { it },
|
intent.addSearchInfo(searchInfo)
|
||||||
): ActivityResultLauncher<Intent> {
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
return this.registerForActivityResult(
|
context.startActivity(intent)
|
||||||
ActivityResultContracts.StartActivityForResult()
|
}
|
||||||
) {
|
|
||||||
setActivityResult(
|
fun startActivityForSelectionModeResult(
|
||||||
lockDatabase,
|
context: Context,
|
||||||
it.resultCode,
|
intent: Intent,
|
||||||
dataTransformation(it.data)
|
typeMode: TypeMode,
|
||||||
)
|
searchInfo: SearchInfo?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
|
) {
|
||||||
|
intent.addSpecialMode(SpecialMode.SELECTION)
|
||||||
|
intent.addTypeMode(typeMode)
|
||||||
|
intent.addSearchInfo(searchInfo)
|
||||||
|
if (activityResultLauncher == null) {
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
}
|
}
|
||||||
}
|
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
|
||||||
|
|
||||||
fun startActivityForSearchModeResult(context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForSaveModeResult(context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SAVE)
|
|
||||||
addTypeModeInIntent(intent, TypeMode.DEFAULT)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForKeyboardSelectionModeResult(context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
|
||||||
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to start an activity with an Autofill for result
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
fun startActivityForAutofillSelectionModeResult(
|
|
||||||
context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo?
|
|
||||||
) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
|
||||||
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
|
||||||
intent.addAutofillComponent(context, autofillComponent)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
activityResultLauncher?.launch(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
fun startActivityForPasskeySelectionModeResult(
|
|
||||||
context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
searchInfo: SearchInfo?
|
|
||||||
) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
|
||||||
addTypeModeInIntent(intent, TypeMode.PASSKEY)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
activityResultLauncher?.launch(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startActivityForRegistrationModeResult(
|
fun startActivityForRegistrationModeResult(
|
||||||
context: Context?,
|
context: Context,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
registerInfo: RegisterInfo?,
|
registerInfo: RegisterInfo?,
|
||||||
typeMode: TypeMode
|
typeMode: TypeMode
|
||||||
) {
|
) {
|
||||||
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
intent.addSpecialMode(SpecialMode.REGISTRATION)
|
||||||
addTypeModeInIntent(intent, typeMode)
|
intent.addTypeMode(typeMode)
|
||||||
addRegisterInfoInIntent(intent, registerInfo)
|
intent.addRegisterInfo(registerInfo)
|
||||||
if (activityResultLauncher == null) {
|
if (activityResultLauncher == null) {
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
}
|
}
|
||||||
activityResultLauncher?.launch(intent) ?: context?.startActivity(intent) ?:
|
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
|
||||||
throw IllegalStateException("At least Context or ActivityResultLauncher must not be null")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
/**
|
||||||
|
* Build the special mode response for internal entry selection for one entry
|
||||||
|
*/
|
||||||
|
fun Activity.buildSpecialModeResponseAndSetResult(
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
this.buildSpecialModeResponseAndSetResult(listOf(entryInfo), extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the special mode response for internal entry selection for multiple entries
|
||||||
|
*/
|
||||||
|
fun Activity.buildSpecialModeResponseAndSetResult(
|
||||||
|
entriesInfo: List<EntryInfo>,
|
||||||
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(javaClass.name, "Success special mode manual selection")
|
||||||
|
mReplyIntent.addNodesIds(entriesInfo.map { it.id })
|
||||||
|
extras?.let {
|
||||||
|
mReplyIntent.putExtras(it)
|
||||||
|
}
|
||||||
|
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.name, "Unable to add the result", e)
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addSearchInfo(searchInfo: SearchInfo?): Intent {
|
||||||
searchInfo?.let {
|
searchInfo?.let {
|
||||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
putExtra(KEY_SEARCH_INFO, it)
|
||||||
}
|
}
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
fun Bundle.addSearchInfo(searchInfo: SearchInfo?): Bundle {
|
||||||
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO)
|
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 {
|
registerInfo?.let {
|
||||||
intent.putExtra(KEY_REGISTER_INFO, it)
|
putExtra(KEY_REGISTER_INFO, it)
|
||||||
}
|
}
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
fun Bundle.addRegisterInfo(registerInfo: RegisterInfo?): Bundle {
|
||||||
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
|
registerInfo?.let {
|
||||||
|
putParcelable(KEY_REGISTER_INFO, it)
|
||||||
|
}
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeInfoFromIntent(intent: Intent) {
|
fun Intent.retrieveRegisterInfo(): RegisterInfo? {
|
||||||
intent.removeExtra(KEY_SEARCH_INFO)
|
return getParcelableExtraCompat(KEY_REGISTER_INFO)
|
||||||
intent.removeExtra(KEY_REGISTER_INFO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
fun Bundle.getRegisterInfo(): RegisterInfo? {
|
||||||
// TODO Replace by Intent.addSpecialMode
|
return getParcelableCompat(KEY_REGISTER_INFO)
|
||||||
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Intent.removeInfo() {
|
||||||
|
removeExtra(KEY_SEARCH_INFO)
|
||||||
|
removeExtra(KEY_REGISTER_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
|
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
|
||||||
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
fun Bundle.addSpecialMode(specialMode: SpecialMode): Bundle {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
this.putEnum(KEY_SPECIAL_MODE, specialMode)
|
||||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
return this
|
||||||
return SpecialMode.SELECTION
|
|
||||||
}
|
|
||||||
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
fun Intent.retrieveSpecialMode(): SpecialMode {
|
||||||
// TODO Replace by Intent.addTypeMode
|
return this.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||||
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Bundle.getSpecialMode(): SpecialMode {
|
||||||
|
return this.getEnum<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
|
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
|
||||||
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
fun Intent.retrieveTypeMode(): TypeMode {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
return getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
|
||||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
|
||||||
return TypeMode.AUTOFILL
|
|
||||||
}
|
|
||||||
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeModesFromIntent(intent: Intent) {
|
fun Intent.removeModes() {
|
||||||
intent.removeExtra(KEY_SPECIAL_MODE)
|
removeExtra(KEY_SPECIAL_MODE)
|
||||||
intent.removeExtra(KEY_TYPE_MODE)
|
removeExtra(KEY_TYPE_MODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addNodesIds(nodesIds: List<UUID>): Intent {
|
||||||
|
this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrieveNodesIds(): List<UUID>? {
|
||||||
|
return getParcelableList<ParcelUuid>(EXTRA_NODES_IDS)?.map { it.uuid }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.removeNodesIds() {
|
||||||
|
removeExtra(EXTRA_NODES_IDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the node id to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addNodeId(nodeId: UUID?) {
|
||||||
|
nodeId?.let {
|
||||||
|
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the node id from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveNodeId(): UUID? {
|
||||||
|
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.removeNodeId() {
|
||||||
|
removeExtra(EXTRA_NODE_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,103 +288,101 @@ object EntrySelectionHelper {
|
|||||||
*/
|
*/
|
||||||
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
|
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
|
||||||
return (specialMode == SpecialMode.SELECTION
|
return (specialMode == SpecialMode.SELECTION
|
||||||
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
&& (typeMode == TypeMode.MAGIKEYBOARD || typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||||
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
|
||||||
|| (specialMode == SpecialMode.REGISTRATION
|
|| (specialMode == SpecialMode.REGISTRATION
|
||||||
&& typeMode == TypeMode.PASSKEY)
|
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doSpecialAction(intent: Intent,
|
fun doSpecialAction(
|
||||||
defaultAction: () -> Unit,
|
intent: Intent,
|
||||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
defaultAction: () -> Unit,
|
||||||
saveAction: (searchInfo: SearchInfo) -> Unit,
|
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
selectionAction: (
|
||||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
intentSenderMode: Boolean,
|
||||||
autofillComponent: AutofillComponent) -> Unit,
|
typeMode: TypeMode,
|
||||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit,
|
searchInfo: SearchInfo?
|
||||||
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
) -> Unit,
|
||||||
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
registrationAction: (
|
||||||
|
intentSenderMode: Boolean,
|
||||||
when (retrieveSpecialModeFromIntent(intent)) {
|
typeMode: TypeMode,
|
||||||
|
registerInfo: RegisterInfo?
|
||||||
|
) -> Unit
|
||||||
|
) {
|
||||||
|
when (val specialMode = intent.retrieveSpecialMode()) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
defaultAction.invoke()
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
SpecialMode.SEARCH -> {
|
SpecialMode.SEARCH -> {
|
||||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
val searchInfo = intent.retrieveSearchInfo()
|
||||||
removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
if (searchInfo != null)
|
if (searchInfo != null)
|
||||||
searchAction.invoke(searchInfo)
|
searchAction.invoke(searchInfo)
|
||||||
else {
|
else {
|
||||||
defaultAction.invoke()
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpecialMode.SAVE -> {
|
|
||||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
|
||||||
removeModesFromIntent(intent)
|
|
||||||
removeInfoFromIntent(intent)
|
|
||||||
if (searchInfo != null)
|
|
||||||
saveAction.invoke(searchInfo)
|
|
||||||
else {
|
|
||||||
defaultAction.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SpecialMode.SELECTION -> {
|
SpecialMode.SELECTION -> {
|
||||||
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
|
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
|
||||||
var autofillComponentInit = false
|
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
when (val typeMode = intent.retrieveTypeMode()) {
|
||||||
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
|
TypeMode.DEFAULT -> {
|
||||||
autofillSelectionAction.invoke(searchInfo, autofillComponent)
|
intent.removeModes()
|
||||||
autofillComponentInit = true
|
if (searchInfo != null)
|
||||||
}
|
searchAction.invoke(searchInfo)
|
||||||
}
|
else
|
||||||
if (!autofillComponentInit) {
|
defaultAction.invoke()
|
||||||
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
}
|
||||||
when (retrieveTypeModeFromIntent(intent)) {
|
TypeMode.MAGIKEYBOARD -> selectionAction.invoke(
|
||||||
TypeMode.DEFAULT -> {
|
isIntentSenderMode(specialMode, typeMode),
|
||||||
removeModesFromIntent(intent)
|
typeMode,
|
||||||
if (searchInfo != null)
|
searchInfo
|
||||||
searchAction.invoke(searchInfo)
|
)
|
||||||
else
|
TypeMode.PASSKEY ->
|
||||||
defaultAction.invoke()
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
}
|
selectionAction.invoke(
|
||||||
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
isIntentSenderMode(specialMode, typeMode),
|
||||||
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo)
|
typeMode,
|
||||||
else -> {
|
searchInfo
|
||||||
// In this case, error
|
)
|
||||||
removeModesFromIntent(intent)
|
} else
|
||||||
removeInfoFromIntent(intent)
|
defaultAction.invoke()
|
||||||
}
|
TypeMode.AUTOFILL -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
selectionAction.invoke(
|
||||||
|
isIntentSenderMode(specialMode, typeMode),
|
||||||
|
typeMode,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
} else
|
||||||
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (searchInfo != null)
|
|
||||||
searchAction.invoke(searchInfo)
|
|
||||||
else
|
|
||||||
defaultAction.invoke()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (searchInfo != null)
|
||||||
|
searchAction.invoke(searchInfo)
|
||||||
|
else
|
||||||
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
|
||||||
if (!isIntentSenderMode(
|
val typeMode = intent.retrieveTypeMode()
|
||||||
specialMode = retrieveSpecialModeFromIntent(intent),
|
val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
|
||||||
typeMode = retrieveTypeModeFromIntent(intent))
|
if (!intentSenderMode) {
|
||||||
) {
|
intent.removeModes()
|
||||||
removeModesFromIntent(intent)
|
intent.removeInfo()
|
||||||
removeInfoFromIntent(intent)
|
|
||||||
}
|
}
|
||||||
when (retrieveTypeModeFromIntent(intent)) {
|
if (registerInfo != null)
|
||||||
TypeMode.AUTOFILL -> {
|
registrationAction.invoke(
|
||||||
autofillRegistrationAction.invoke(registerInfo)
|
intentSenderMode,
|
||||||
}
|
typeMode,
|
||||||
TypeMode.PASSKEY -> {
|
registerInfo
|
||||||
passkeyRegistrationAction.invoke(registerInfo)
|
)
|
||||||
}
|
else {
|
||||||
else -> {
|
defaultAction.invoke()
|
||||||
// Do other registration type
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,7 +414,7 @@ object EntrySelectionHelper {
|
|||||||
try {
|
try {
|
||||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
return Icon.createWithBitmap(bitmap)
|
return IconCompat.createWithBitmap(bitmap).toIcon(context)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.credentialprovider
|
|||||||
enum class SpecialMode {
|
enum class SpecialMode {
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
SEARCH,
|
SEARCH,
|
||||||
SAVE,
|
|
||||||
SELECTION,
|
SELECTION,
|
||||||
REGISTRATION;
|
REGISTRATION;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
enum class TypeMode {
|
enum class TypeMode {
|
||||||
DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY
|
DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
|
||||||
}
|
}
|
||||||
@@ -27,245 +27,186 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.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.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.retrieveAutofillComponent
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
|
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.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.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.utils.AppUtil
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|
||||||
import com.kunzisoft.keepass.view.toastError
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
|
||||||
this.buildActivityResultLauncher(lockDatabase = true)
|
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
return false
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
}
|
autofillLauncherViewModel.manageSelectionResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
return true
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
}
|
autofillLauncherViewModel.manageRegistrationResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun applyCustomStyle(): Boolean = false
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
// Retrieve selection mode
|
override fun finishActivityIfReloadRequested(): Boolean = true
|
||||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
|
||||||
when (specialMode) {
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
SpecialMode.SELECTION -> {
|
|
||||||
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
// To pass extra inline request
|
// To apply the bypass https://github.com/Kunzisoft/KeePassDX/issues/2238
|
||||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
// before managing intent in super class
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
intent.retrieveSelectionBundle()?.apply {
|
||||||
compatInlineSuggestionsRequest = bundle.getParcelableCompat(
|
intent.addSpecialMode(getSpecialMode())
|
||||||
KEY_INLINE_SUGGESTION
|
intent.addSearchInfo(getSearchInfo())
|
||||||
)
|
intent.addRegisterInfo(getRegisterInfo())
|
||||||
}
|
intent.addAutofillComponent(retrieveAutofillComponent())
|
||||||
// Build search param
|
}
|
||||||
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
super.onCreate(savedInstanceState)
|
||||||
AppUtil.getConcreteWebDomain(
|
autofillLauncherViewModel.initialize()
|
||||||
this,
|
lifecycleScope.launch {
|
||||||
searchInfo.webDomain
|
// Initialize the parameters
|
||||||
) { concreteWebDomain ->
|
autofillLauncherViewModel.uiState.collect { uiState ->
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
when (uiState) {
|
||||||
val assistStructure = AutofillHelper
|
AutofillLauncherViewModel.UIState.Loading -> {}
|
||||||
.retrieveAutofillComponent(intent)
|
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
|
||||||
?.assistStructure
|
showBlockRestartMessage()
|
||||||
val newAutofillComponent = if (assistStructure != null) {
|
autofillLauncherViewModel.cancelResult()
|
||||||
AutofillComponent(
|
|
||||||
assistStructure,
|
|
||||||
compatInlineSuggestionsRequest
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
searchInfo.webDomain = concreteWebDomain
|
|
||||||
launchSelection(database, newAutofillComponent, searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Remove bundle
|
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
|
||||||
intent.removeExtra(KEY_SELECTION_BUNDLE)
|
showAutofillSuggestionMessage()
|
||||||
}
|
|
||||||
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)
|
lifecycleScope.launch {
|
||||||
finish()
|
// 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this@AutofillLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mAutofillRegistrationActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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@AutofillLauncherActivity,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = uiState.lockDatabase,
|
||||||
|
resultCode = uiState.resultCode,
|
||||||
|
data = uiState.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.ShowError -> {
|
||||||
|
toastError(uiState.error)
|
||||||
|
autofillLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchSelection(database: ContextualDatabase?,
|
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
autofillComponent: AutofillComponent?,
|
super.onUnknownDatabaseRetrieved(database)
|
||||||
searchInfo: SearchInfo) {
|
autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||||
if (autofillComponent == null) {
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
} else if (KeeAutofillService.autofillAllowedFor(
|
|
||||||
applicationId = searchInfo.applicationId,
|
|
||||||
webDomain = searchInfo.webDomain,
|
|
||||||
context = this
|
|
||||||
)) {
|
|
||||||
// If database is open
|
|
||||||
SearchHelper.checkAutoSearchInfo(
|
|
||||||
context = this,
|
|
||||||
database = database,
|
|
||||||
searchInfo = searchInfo,
|
|
||||||
onItemsFound = { openedDatabase, items ->
|
|
||||||
// Items found
|
|
||||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
onItemNotFound = { openedDatabase ->
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
GroupActivity.launchForAutofillSelectionResult(
|
|
||||||
this,
|
|
||||||
openedDatabase,
|
|
||||||
mCredentialActivityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onDatabaseClosed = {
|
|
||||||
// If database not open
|
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(
|
|
||||||
this,
|
|
||||||
mCredentialActivityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showBlockRestartMessage()
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchRegistration(database: ContextualDatabase?,
|
|
||||||
searchInfo: SearchInfo,
|
|
||||||
registerInfo: RegisterInfo?) {
|
|
||||||
if (KeeAutofillService.autofillAllowedFor(
|
|
||||||
applicationId = searchInfo.applicationId,
|
|
||||||
webDomain = searchInfo.webDomain,
|
|
||||||
context = this
|
|
||||||
)) {
|
|
||||||
val readOnly = database?.isReadOnly != false
|
|
||||||
SearchHelper.checkAutoSearchInfo(
|
|
||||||
context = this,
|
|
||||||
database = database,
|
|
||||||
searchInfo = searchInfo,
|
|
||||||
onItemsFound = { openedDatabase, _ ->
|
|
||||||
if (!readOnly) {
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
GroupActivity.launchForRegistration(
|
|
||||||
context = this,
|
|
||||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
|
||||||
database = openedDatabase,
|
|
||||||
registerInfo = registerInfo,
|
|
||||||
typeMode = TypeMode.AUTOFILL
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showReadOnlySaveMessage()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onItemNotFound = { openedDatabase ->
|
|
||||||
if (!readOnly) {
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
GroupActivity.launchForRegistration(
|
|
||||||
context = this,
|
|
||||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
|
||||||
database = openedDatabase,
|
|
||||||
registerInfo = registerInfo,
|
|
||||||
typeMode = TypeMode.AUTOFILL
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showReadOnlySaveMessage()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDatabaseClosed = {
|
|
||||||
// If database not open
|
|
||||||
FileDatabaseSelectActivity.launchForRegistration(
|
|
||||||
context = this,
|
|
||||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
|
||||||
registerInfo = registerInfo,
|
|
||||||
typeMode = TypeMode.AUTOFILL
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showBlockRestartMessage()
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showBlockRestartMessage() {
|
private fun showBlockRestartMessage() {
|
||||||
// If item not allowed, show a toast
|
// If item not allowed, show a toast
|
||||||
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.autofill_block_restart,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showReadOnlySaveMessage() {
|
private fun showAutofillSuggestionMessage() {
|
||||||
toastError(RegisterInReadOnlyDatabaseException())
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.autofill_inline_suggestions_keyboard,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_PENDING_INTENT_BUNDLE = "com.kunzisoft.keepass.extra.BUNDLE"
|
||||||
private val TAG = AutofillLauncherActivity::class.java.name
|
private val TAG = AutofillLauncherActivity::class.java.name
|
||||||
|
|
||||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
fun Intent.retrieveSelectionBundle(): Bundle? {
|
||||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
return this.getBundleExtra(KEY_PENDING_INTENT_BUNDLE)
|
||||||
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
}
|
||||||
|
|
||||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
fun getPendingIntentForSelection(
|
||||||
|
context: Context,
|
||||||
fun getPendingIntentForSelection(context: Context,
|
searchInfo: SearchInfo? = null,
|
||||||
searchInfo: SearchInfo? = null,
|
autofillComponent: AutofillComponent
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? {
|
): PendingIntent? {
|
||||||
try {
|
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(
|
return PendingIntent.getActivity(
|
||||||
context, 0,
|
context,
|
||||||
// Doesn't work with direct extra Parcelable (don't know why?)
|
randomRequestCode(),
|
||||||
// Wrap into a bundle to bypass the problem
|
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
|
||||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
@@ -279,14 +220,21 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPendingIntentForRegistration(context: Context,
|
fun getPendingIntentForRegistration(
|
||||||
registerInfo: RegisterInfo): PendingIntent? {
|
context: Context,
|
||||||
|
registerInfo: RegisterInfo
|
||||||
|
): PendingIntent? {
|
||||||
try {
|
try {
|
||||||
|
// Bypass intent issue
|
||||||
|
val tempBundle = Bundle().apply {
|
||||||
|
addSpecialMode(SpecialMode.REGISTRATION)
|
||||||
|
addRegisterInfo(registerInfo)
|
||||||
|
}
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
context, 0,
|
context,
|
||||||
|
randomRequestCode(),
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
|
||||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
|
||||||
},
|
},
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
@@ -299,14 +247,5 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchForRegistration(context: Context,
|
|
||||||
registerInfo: RegisterInfo) {
|
|
||||||
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
|
||||||
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,22 +22,22 @@ package com.kunzisoft.keepass.credentialprovider.activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.core.net.toUri
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.kunzisoft.keepass.R
|
import androidx.activity.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
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.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.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 com.kunzisoft.keepass.view.toastError
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
@@ -45,198 +45,131 @@ import com.kunzisoft.keepass.view.toastError
|
|||||||
*/
|
*/
|
||||||
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
private val entrySelectionViewModel: EntrySelectionViewModel by viewModels()
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
return false
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
}
|
entrySelectionViewModel.manageSelectionResult(it)
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchSelection(database: ContextualDatabase?,
|
|
||||||
sharedWebDomain: String?,
|
|
||||||
otpString: String?) {
|
|
||||||
// Build domain search param
|
|
||||||
val searchInfo = SearchInfo().apply {
|
|
||||||
this.webDomain = sharedWebDomain
|
|
||||||
this.otpString = otpString
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
override fun applyCustomStyle() = false
|
||||||
searchInfo.webDomain = concreteWebDomain
|
|
||||||
launch(database, searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launch(database: ContextualDatabase?,
|
override fun finishActivityIfReloadRequested() = false
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
|
|
||||||
// Setting to integrate Magikeyboard
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
|
|
||||||
|
|
||||||
// If database is open
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val readOnly = database?.isReadOnly != false
|
super.onCreate(savedInstanceState)
|
||||||
SearchHelper.checkAutoSearchInfo(
|
entrySelectionViewModel.initialize()
|
||||||
context = this,
|
lifecycleScope.launch {
|
||||||
database = database,
|
// Initialize the parameters
|
||||||
searchInfo = searchInfo,
|
entrySelectionViewModel.uiState.collect { uiState ->
|
||||||
onItemsFound = { openedDatabase, items ->
|
when (uiState) {
|
||||||
// Items found
|
is EntrySelectionViewModel.UIState.Loading -> {}
|
||||||
if (searchInfo.otpString != null) {
|
is EntrySelectionViewModel.UIState.PopulateKeyboard -> {
|
||||||
if (!readOnly) {
|
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(
|
||||||
GroupActivity.launchForSaveResult(
|
context = this@EntrySelectionLauncherActivity,
|
||||||
this,
|
entry = uiState.entryInfo,
|
||||||
openedDatabase,
|
toast = true
|
||||||
searchInfo,
|
|
||||||
false
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
toastError(RegisterInReadOnlyDatabaseException())
|
|
||||||
}
|
}
|
||||||
} else if (searchShareForMagikeyboard) {
|
is EntrySelectionViewModel.UIState.LaunchFileDatabaseSelectForSearch -> {
|
||||||
MagikeyboardService.performSelection(
|
FileDatabaseSelectActivity.launchForSearch(
|
||||||
items,
|
context = this@EntrySelectionLauncherActivity,
|
||||||
{ entryInfo ->
|
searchInfo = uiState.searchInfo
|
||||||
// Automatically populate keyboard
|
)
|
||||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
}
|
||||||
this,
|
is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> {
|
||||||
entryInfo
|
GroupActivity.launchForSearch(
|
||||||
)
|
context = this@EntrySelectionLauncherActivity,
|
||||||
},
|
database = uiState.database,
|
||||||
{ autoSearch ->
|
searchInfo = uiState.searchInfo
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onUnknownDatabaseRetrieved(database)
|
||||||
|
entrySelectionViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
fun launch(
|
||||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
context: Context,
|
||||||
|
searchInfo: SearchInfo? = null
|
||||||
fun launch(context: Context,
|
) {
|
||||||
searchInfo: SearchInfo? = null) {
|
context.startActivity(Intent(
|
||||||
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
|
context,
|
||||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
EntrySelectionLauncherActivity::class.java
|
||||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
).apply {
|
||||||
})
|
addSearchInfo(searchInfo)
|
||||||
}
|
// New task needed because don't launch from an Activity context
|
||||||
// New task needed because don't launch from an Activity context
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,8 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
@@ -44,14 +46,14 @@ import com.kunzisoft.keepass.credentialprovider.TypeMode
|
|||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.model.AppOrigin
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
import com.kunzisoft.keepass.view.toastError
|
import com.kunzisoft.keepass.view.toastError
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -79,10 +81,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -105,61 +103,69 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
nodeId = uiState.nodeId
|
nodeId = uiState.nodeId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is PasskeyLauncherViewModel.UIState.SetActivityResult -> {
|
|
||||||
setActivityResult(
|
|
||||||
lockDatabase = uiState.lockDatabase,
|
|
||||||
resultCode = uiState.resultCode,
|
|
||||||
data = uiState.data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.ShowError -> {
|
|
||||||
toastError(uiState.error)
|
|
||||||
passkeyLauncherViewModel.cancelResult()
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
|
|
||||||
GroupActivity.launchForPasskeySelectionResult(
|
|
||||||
context = this@PasskeyLauncherActivity,
|
|
||||||
database = uiState.database,
|
|
||||||
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
|
||||||
searchInfo = null,
|
|
||||||
autoSearch = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
|
|
||||||
GroupActivity.launchForRegistration(
|
|
||||||
context = this@PasskeyLauncherActivity,
|
|
||||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
|
||||||
database = uiState.database,
|
|
||||||
registerInfo = uiState.registerInfo,
|
|
||||||
typeMode = uiState.typeMode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
|
|
||||||
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
|
|
||||||
activity = this@PasskeyLauncherActivity,
|
|
||||||
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
|
||||||
searchInfo = uiState.searchInfo,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
|
||||||
FileDatabaseSelectActivity.launchForRegistration(
|
|
||||||
context = this@PasskeyLauncherActivity,
|
|
||||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
|
||||||
registerInfo = uiState.registerInfo,
|
|
||||||
typeMode = uiState.typeMode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
|
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
|
||||||
updateEntry(uiState.oldEntry, uiState.newEntry)
|
updateEntry(uiState.oldEntry, uiState.newEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
passkeyLauncherViewModel.credentialUiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is CredentialLauncherViewModel.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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onUnknownDatabaseRetrieved(database)
|
||||||
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database)
|
passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
@@ -170,7 +176,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
passkeyLauncherViewModel.autoSelectPasskey(result, database)
|
// TODO When auto save is enabled, WARNING filter by the calling activity
|
||||||
|
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,6 +241,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
)
|
)
|
||||||
.append("\n\n")
|
.append("\n\n")
|
||||||
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
|
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
|
||||||
|
.append("\n\n")
|
||||||
|
.append(getString(R.string.passkeys_missing_signature_app_ask_question))
|
||||||
.toString()
|
.toString()
|
||||||
)
|
)
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
@@ -273,7 +282,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
): PendingIntent? {
|
): PendingIntent? {
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
(Math.random() * Integer.MAX_VALUE).toInt(),
|
randomRequestCode(),
|
||||||
Intent(context, PasskeyLauncherActivity::class.java).apply {
|
Intent(context, PasskeyLauncherActivity::class.java).apply {
|
||||||
addSpecialMode(specialMode)
|
addSpecialMode(specialMode)
|
||||||
addTypeMode(TypeMode.PASSKEY)
|
addTypeMode(TypeMode.PASSKEY)
|
||||||
|
|||||||
@@ -2,5 +2,7 @@ package com.kunzisoft.keepass.credentialprovider.autofill
|
|||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
|
|
||||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
data class AutofillComponent(
|
||||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
val assistStructure: AssistStructure,
|
||||||
|
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?
|
||||||
|
)
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -28,6 +27,7 @@ import android.content.Intent
|
|||||||
import android.graphics.BlendMode
|
import android.graphics.BlendMode
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import android.service.autofill.Dataset
|
import android.service.autofill.Dataset
|
||||||
import android.service.autofill.Field
|
import android.service.autofill.Field
|
||||||
import android.service.autofill.FillResponse
|
import android.service.autofill.FillResponse
|
||||||
@@ -38,7 +38,6 @@ import android.view.autofill.AutofillId
|
|||||||
import android.view.autofill.AutofillManager
|
import android.view.autofill.AutofillManager
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import android.widget.Toast
|
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
@@ -54,21 +53,63 @@ import com.kunzisoft.keepass.model.EntryInfo
|
|||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import java.io.IOException
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
object AutofillHelper {
|
object AutofillHelper {
|
||||||
|
|
||||||
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
private const val EXTRA_BASE_STRUCTURE = "com.kunzisoft.keepass.autofill.BASE_STRUCTURE"
|
||||||
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||||
|
|
||||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
fun Intent.addAutofillComponent(autofillComponent: AutofillComponent?): Intent {
|
||||||
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
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) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
AutofillComponent(assistStructure,
|
AutofillComponent(
|
||||||
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
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 {
|
} else {
|
||||||
AutofillComponent(assistStructure, null)
|
AutofillComponent(assistStructure, null)
|
||||||
}
|
}
|
||||||
@@ -127,11 +168,13 @@ object AutofillHelper {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDatasetForEntry(context: Context,
|
private fun buildDatasetForEntry(
|
||||||
database: ContextualDatabase,
|
context: Context,
|
||||||
entryInfo: EntryInfo,
|
database: ContextualDatabase,
|
||||||
struct: StructureParser.Result,
|
entryInfo: EntryInfo,
|
||||||
inlinePresentation: InlinePresentation?): Dataset {
|
struct: StructureParser.Result,
|
||||||
|
inlinePresentation: InlinePresentation?
|
||||||
|
): Dataset {
|
||||||
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
|
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
|
||||||
|
|
||||||
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
@@ -291,11 +334,13 @@ object AutofillHelper {
|
|||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(
|
||||||
database: ContextualDatabase,
|
context: Context,
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
database: ContextualDatabase,
|
||||||
positionItem: Int,
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||||
entryInfo: EntryInfo): InlinePresentation? {
|
positionItem: Int,
|
||||||
|
entryInfo: EntryInfo
|
||||||
|
): InlinePresentation? {
|
||||||
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||||
@@ -314,7 +359,7 @@ object AutofillHelper {
|
|||||||
// Build the content for IME UI
|
// Build the content for IME UI
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent(context, AutofillSettingsActivity::class.java),
|
Intent(context, AutofillSettingsActivity::class.java),
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
@@ -341,9 +386,11 @@ object AutofillHelper {
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun buildInlinePresentationForManualSelection(context: Context,
|
private fun buildInlinePresentationForManualSelection(
|
||||||
inlinePresentationSpec: InlinePresentationSpec,
|
context: Context,
|
||||||
pendingIntent: PendingIntent): InlinePresentation? {
|
inlinePresentationSpec: InlinePresentationSpec,
|
||||||
|
pendingIntent: PendingIntent
|
||||||
|
): InlinePresentation? {
|
||||||
// Make sure that the IME spec claims support for v1 UI template.
|
// Make sure that the IME spec claims support for v1 UI template.
|
||||||
val imeStyle = inlinePresentationSpec.style
|
val imeStyle = inlinePresentationSpec.style
|
||||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||||
@@ -360,11 +407,13 @@ object AutofillHelper {
|
|||||||
}.build().slice, inlinePresentationSpec, false)
|
}.build().slice, inlinePresentationSpec, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildResponse(context: Context,
|
fun buildResponse(
|
||||||
database: ContextualDatabase,
|
context: Context,
|
||||||
entriesInfo: List<EntryInfo>,
|
database: ContextualDatabase,
|
||||||
parseResult: StructureParser.Result,
|
entriesInfo: List<EntryInfo>,
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
parseResult: StructureParser.Result,
|
||||||
|
autofillComponent: AutofillComponent
|
||||||
|
): FillResponse? {
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
// Add Header
|
// Add Header
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
@@ -385,7 +434,8 @@ object AutofillHelper {
|
|||||||
// Add inline suggestion for new IME and dataset
|
// Add inline suggestion for new IME and dataset
|
||||||
var numberInlineSuggestions = 0
|
var numberInlineSuggestions = 0
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
autofillComponent.compatInlineSuggestionsRequest
|
||||||
|
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||||
@@ -401,21 +451,27 @@ object AutofillHelper {
|
|||||||
var inlinePresentation: InlinePresentation? = null
|
var inlinePresentation: InlinePresentation? = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& numberInlineSuggestions > 0
|
&& numberInlineSuggestions > 0
|
||||||
&& compatInlineSuggestionsRequest != null) {
|
&& autofillComponent.compatInlineSuggestionsRequest != null) {
|
||||||
inlinePresentation = buildInlinePresentationForEntry(
|
inlinePresentation = buildInlinePresentationForEntry(
|
||||||
context,
|
context,
|
||||||
database,
|
database,
|
||||||
compatInlineSuggestionsRequest,
|
autofillComponent.compatInlineSuggestionsRequest,
|
||||||
numberInlineSuggestions--,
|
numberInlineSuggestions--,
|
||||||
entry
|
entry
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Create dataset for each entry
|
// Create dataset for each entry
|
||||||
responseBuilder.addDataset(
|
responseBuilder.addDataset(
|
||||||
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
|
buildDatasetForEntry(
|
||||||
|
context = context,
|
||||||
|
database = database,
|
||||||
|
entryInfo = entry,
|
||||||
|
struct = parseResult,
|
||||||
|
inlinePresentation = inlinePresentation
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to add dataset")
|
Log.e(TAG, "Unable to add dataset", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,21 +483,28 @@ object AutofillHelper {
|
|||||||
webScheme = parseResult.webScheme
|
webScheme = parseResult.webScheme
|
||||||
manualSelection = true
|
manualSelection = true
|
||||||
}
|
}
|
||||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
val manualSelectionView = RemoteViews(
|
||||||
AutofillLauncherActivity.getPendingIntentForSelection(context,
|
context.packageName,
|
||||||
searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent ->
|
R.layout.item_autofill_select_entry
|
||||||
|
)
|
||||||
|
AutofillLauncherActivity.getPendingIntentForSelection(
|
||||||
|
context,
|
||||||
|
searchInfo,
|
||||||
|
autofillComponent
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
|
||||||
var inlinePresentation: InlinePresentation? = null
|
var inlinePresentation: InlinePresentation? = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
autofillComponent.compatInlineSuggestionsRequest
|
||||||
val inlinePresentationSpec =
|
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
val inlinePresentationSpec =
|
||||||
inlinePresentation = buildInlinePresentationForManualSelection(
|
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||||
context,
|
inlinePresentation = buildInlinePresentationForManualSelection(
|
||||||
inlinePresentationSpec,
|
context,
|
||||||
pendingIntent
|
inlinePresentationSpec,
|
||||||
)
|
pendingIntent
|
||||||
}
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
@@ -486,61 +549,31 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the Autofill response for one entry
|
* Build the Autofill response
|
||||||
*/
|
*/
|
||||||
fun buildResponseAndSetResult(activity: Activity,
|
fun buildResponse(
|
||||||
database: ContextualDatabase,
|
context: Context,
|
||||||
entryInfo: EntryInfo) {
|
autofillComponent: AutofillComponent,
|
||||||
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
database: ContextualDatabase,
|
||||||
}
|
entriesInfo: List<EntryInfo>,
|
||||||
|
onIntentCreated: (Intent) -> Unit
|
||||||
/**
|
) {
|
||||||
* Build the Autofill response for many entry
|
|
||||||
*/
|
|
||||||
fun buildResponseAndSetResult(activity: Activity,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entriesInfo: List<EntryInfo>) {
|
|
||||||
if (entriesInfo.isEmpty()) {
|
if (entriesInfo.isEmpty()) {
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
throw IOException("No entries found")
|
||||||
} else {
|
} else {
|
||||||
var setResultOk = false
|
StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
|
||||||
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
// New Response
|
||||||
StructureParser(structure).parse()?.let { result ->
|
onIntentCreated(Intent().putExtra(
|
||||||
// New Response
|
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
buildResponse(
|
||||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(
|
context = context,
|
||||||
EXTRA_INLINE_SUGGESTIONS_REQUEST
|
database = database,
|
||||||
)
|
entriesInfo = entriesInfo,
|
||||||
if (compatInlineSuggestionsRequest != null) {
|
parseResult = result,
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
autofillComponent = autofillComponent
|
||||||
}
|
)
|
||||||
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
|
))
|
||||||
} else {
|
} ?: throw IOException("Unable to parse the structure")
|
||||||
buildResponse(activity, database, entriesInfo, result, null)
|
|
||||||
}
|
|
||||||
val mReplyIntent = Intent()
|
|
||||||
Log.d(activity.javaClass.name, "Success Autofill auth.")
|
|
||||||
mReplyIntent.putExtra(
|
|
||||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
|
||||||
response)
|
|
||||||
setResultOk = true
|
|
||||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!setResultOk) {
|
|
||||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
|
|
||||||
this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
|
||||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
|
|
||||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
|
||||||
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
|
|||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.AppUtil
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
|
|
||||||
@@ -92,10 +92,11 @@ class KeeAutofillService : AutofillService() {
|
|||||||
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
|
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFillRequest(request: FillRequest,
|
override fun onFillRequest(
|
||||||
cancellationSignal: CancellationSignal,
|
request: FillRequest,
|
||||||
callback: FillCallback) {
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: FillCallback
|
||||||
|
) {
|
||||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||||
|
|
||||||
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
|
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
|
||||||
@@ -120,67 +121,64 @@ class KeeAutofillService : AutofillService() {
|
|||||||
webDomain = parseResult.webDomain
|
webDomain = parseResult.webDomain
|
||||||
webScheme = parseResult.webScheme
|
webScheme = parseResult.webScheme
|
||||||
}
|
}
|
||||||
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
CompatInlineSuggestionsRequest(request)
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
} else {
|
||||||
CompatInlineSuggestionsRequest(request)
|
null
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
launchSelection(mDatabase,
|
|
||||||
searchInfo,
|
|
||||||
parseResult,
|
|
||||||
inlineSuggestionsRequest,
|
|
||||||
callback)
|
|
||||||
}
|
}
|
||||||
|
val autofillComponent = AutofillComponent(
|
||||||
|
latestStructure,
|
||||||
|
inlineSuggestionsRequest
|
||||||
|
)
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = mDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, items ->
|
||||||
|
callback.onSuccess(
|
||||||
|
AutofillHelper.buildResponse(
|
||||||
|
context = this,
|
||||||
|
database = openedDatabase,
|
||||||
|
entriesInfo = items,
|
||||||
|
parseResult = parseResult,
|
||||||
|
autofillComponent = autofillComponent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onItemNotFound = { openedDatabase ->
|
||||||
|
// Show UI if no search result
|
||||||
|
showUIForEntrySelection(parseResult, openedDatabase,
|
||||||
|
searchInfo, autofillComponent, callback)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Show UI if database not open
|
||||||
|
showUIForEntrySelection(parseResult, null,
|
||||||
|
searchInfo, autofillComponent, callback)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchSelection(database: ContextualDatabase?,
|
|
||||||
searchInfo: SearchInfo,
|
|
||||||
parseResult: StructureParser.Result,
|
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
|
||||||
callback: FillCallback) {
|
|
||||||
SearchHelper.checkAutoSearchInfo(
|
|
||||||
context = this,
|
|
||||||
database = database,
|
|
||||||
searchInfo = searchInfo,
|
|
||||||
onItemsFound = { openedDatabase, items ->
|
|
||||||
callback.onSuccess(
|
|
||||||
AutofillHelper.buildResponse(
|
|
||||||
this, openedDatabase,
|
|
||||||
items, parseResult, inlineSuggestionsRequest
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onItemNotFound = { openedDatabase ->
|
|
||||||
// Show UI if no search result
|
|
||||||
showUIForEntrySelection(parseResult, openedDatabase,
|
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
|
||||||
},
|
|
||||||
onDatabaseClosed = {
|
|
||||||
// Show UI if database not open
|
|
||||||
showUIForEntrySelection(parseResult, null,
|
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
private fun showUIForEntrySelection(
|
||||||
database: ContextualDatabase?,
|
parseResult: StructureParser.Result,
|
||||||
searchInfo: SearchInfo,
|
database: ContextualDatabase?,
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
searchInfo: SearchInfo,
|
||||||
callback: FillCallback) {
|
autofillComponent: AutofillComponent,
|
||||||
|
callback: FillCallback
|
||||||
|
) {
|
||||||
var success = false
|
var success = false
|
||||||
parseResult.allAutofillIds().let { autofillIds ->
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
if (autofillIds.isNotEmpty()) {
|
if (autofillIds.isNotEmpty()) {
|
||||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||||
// to generate Response.
|
// to generate Response.
|
||||||
AutofillLauncherActivity.getPendingIntentForSelection(this,
|
AutofillLauncherActivity.getPendingIntentForSelection(
|
||||||
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender ->
|
this,
|
||||||
|
searchInfo,
|
||||||
|
autofillComponent
|
||||||
|
)?.intentSender?.let { intentSender ->
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
||||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||||
@@ -271,7 +269,8 @@ class KeeAutofillService : AutofillService() {
|
|||||||
&& autofillInlineSuggestionsEnabled
|
&& autofillInlineSuggestionsEnabled
|
||||||
) {
|
) {
|
||||||
var inlinePresentation: InlinePresentation? = null
|
var inlinePresentation: InlinePresentation? = null
|
||||||
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
autofillComponent.compatInlineSuggestionsRequest
|
||||||
|
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val inlinePresentationSpecs =
|
val inlinePresentationSpecs =
|
||||||
inlineSuggestionsRequest.inlinePresentationSpecs
|
inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||||
@@ -289,7 +288,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
InlineSuggestionUi.newContentBuilder(
|
InlineSuggestionUi.newContentBuilder(
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent(this, AutofillSettingsActivity::class.java),
|
Intent(this, AutofillSettingsActivity::class.java),
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
@@ -361,7 +360,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||||
var success = false
|
var success = false
|
||||||
if (askToSaveData) {
|
if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
val latestStructure = request.fillContexts.last().structure
|
val latestStructure = request.fillContexts.last().structure
|
||||||
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||||
|
|
||||||
@@ -387,32 +386,32 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show UI to save data
|
// Show UI to save data
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = parseResult.applicationId
|
||||||
|
webDomain = parseResult.webDomain
|
||||||
|
webScheme = parseResult.webScheme
|
||||||
|
}
|
||||||
val registerInfo = RegisterInfo(
|
val registerInfo = RegisterInfo(
|
||||||
searchInfo = SearchInfo().apply {
|
searchInfo = searchInfo,
|
||||||
applicationId = parseResult.applicationId
|
|
||||||
webDomain = parseResult.webDomain
|
|
||||||
webScheme = parseResult.webScheme
|
|
||||||
},
|
|
||||||
username = parseResult.usernameValue?.textValue?.toString(),
|
username = parseResult.usernameValue?.textValue?.toString(),
|
||||||
password = parseResult.passwordValue?.textValue?.toString(),
|
password = parseResult.passwordValue?.textValue?.toString(),
|
||||||
creditCard =
|
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
|
||||||
CreditCard(
|
CreditCard(
|
||||||
parseResult.creditCardHolder,
|
parseResult.creditCardHolder,
|
||||||
parseResult.creditCardNumber,
|
cardNumber,
|
||||||
expiration,
|
expiration,
|
||||||
parseResult.cardVerificationValue
|
parseResult.cardVerificationValue
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// TODO Callback in each activity #765
|
AutofillLauncherActivity.getPendingIntentForRegistration(
|
||||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
this,
|
||||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
registerInfo
|
||||||
// registerInfo))
|
)?.intentSender?.let { intentSender ->
|
||||||
//} else {
|
success = true
|
||||||
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
callback.onSuccess(intentSender)
|
||||||
success = true
|
}
|
||||||
callback.onSuccess()
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,8 +362,8 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
if (result?.passwordId == null) {
|
if (result?.passwordId == null) {
|
||||||
usernameIdCandidate = autofillId
|
usernameIdCandidate = autofillId
|
||||||
usernameValueCandidate = node.autofillValue
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
|
||||||
}
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||||
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
@@ -461,9 +462,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performSelection(items: List<EntryInfo>,
|
fun performSelection(
|
||||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
items: List<EntryInfo>,
|
||||||
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||||
|
actionEntrySelection: (autoSearch: Boolean) -> Unit
|
||||||
|
) {
|
||||||
EntrySelectionHelper.performSelection(
|
EntrySelectionHelper.performSelection(
|
||||||
items = items,
|
items = items,
|
||||||
actionPopulateCredentialProvider = { itemFound ->
|
actionPopulateCredentialProvider = { itemFound ->
|
||||||
@@ -477,15 +480,5 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
actionEntrySelection = actionEntrySelection
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,22 +93,18 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
|
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
|
||||||
return SearchInfo().apply {
|
return SearchInfo().apply {
|
||||||
this.relyingParty = relyingParty
|
this.relyingParty = relyingParty
|
||||||
this.isAPasskeySearch = true
|
|
||||||
this.query = relyingParty
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBeginGetCredentialRequest(
|
override fun onBeginGetCredentialRequest(
|
||||||
request: BeginGetCredentialRequest,
|
request: BeginGetCredentialRequest,
|
||||||
cancellationSignal: CancellationSignal,
|
cancellationSignal: CancellationSignal,
|
||||||
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
|
||||||
) {
|
) {
|
||||||
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
|
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
|
||||||
try {
|
try {
|
||||||
processGetCredentialsRequest(request)?.let { response ->
|
processGetCredentialsRequest(request) { response ->
|
||||||
callback.onResult(response)
|
callback.onResult(response)
|
||||||
} ?: run {
|
|
||||||
callback.onError(GetCredentialUnknownException())
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
|
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
|
||||||
@@ -116,24 +112,30 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? {
|
private fun processGetCredentialsRequest(
|
||||||
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
|
request: BeginGetCredentialRequest,
|
||||||
|
callback: (BeginGetCredentialResponse?) -> Unit
|
||||||
|
) {
|
||||||
|
var knownOption = false
|
||||||
for (option in request.beginGetCredentialOptions) {
|
for (option in request.beginGetCredentialOptions) {
|
||||||
when (option) {
|
when (option) {
|
||||||
is BeginGetPublicKeyCredentialOption -> {
|
is BeginGetPublicKeyCredentialOption -> {
|
||||||
credentialEntries.addAll(
|
knownOption = true
|
||||||
populatePasskeyData(option)
|
populatePasskeyData(option) { listCredentials ->
|
||||||
)
|
callback(BeginGetCredentialResponse(listCredentials))
|
||||||
return BeginGetCredentialResponse(credentialEntries)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption")
|
if (knownOption.not()) {
|
||||||
return null
|
throw IOException("unknown type of beginGetCredentialOption")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
|
private fun populatePasskeyData(
|
||||||
|
option: BeginGetPublicKeyCredentialOption,
|
||||||
|
callback: (List<CredentialEntry>) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
@@ -169,6 +171,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
},
|
},
|
||||||
onItemNotFound = { _ ->
|
onItemNotFound = { _ ->
|
||||||
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
||||||
@@ -191,6 +194,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
||||||
@@ -213,9 +217,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return passkeyEntries
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBeginCreateCredentialRequest(
|
override fun onBeginCreateCredentialRequest(
|
||||||
@@ -225,7 +229,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
) {
|
) {
|
||||||
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
|
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
|
||||||
try {
|
try {
|
||||||
callback.onResult(processCreateCredentialRequest(request))
|
processCreateCredentialRequest(request) {
|
||||||
|
callback.onResult(BeginCreateCredentialResponse(it))
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
|
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
|
||||||
toastError(e)
|
toastError(e)
|
||||||
@@ -233,15 +239,20 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse {
|
private fun processCreateCredentialRequest(
|
||||||
|
request: BeginCreateCredentialRequest,
|
||||||
|
callback: (List<CreateEntry>) -> Unit
|
||||||
|
) {
|
||||||
when (request) {
|
when (request) {
|
||||||
is BeginCreatePublicKeyCredentialRequest -> {
|
is BeginCreatePublicKeyCredentialRequest -> {
|
||||||
// Request is passkey type
|
// Request is passkey type
|
||||||
return handleCreatePasskeyQuery(request)
|
handleCreatePasskeyQuery(request, callback)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// request type not supported
|
||||||
|
throw IOException("unknown type of BeginCreateCredentialRequest")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// request type not supported
|
|
||||||
throw IOException("unknown type of BeginCreateCredentialRequest")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
||||||
@@ -266,9 +277,15 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse {
|
private fun handleCreatePasskeyQuery(
|
||||||
|
request: BeginCreatePublicKeyCredentialRequest,
|
||||||
val accountName = mDatabase?.name ?: getString(R.string.passkey_database_username)
|
callback: (List<CreateEntry>) -> Unit
|
||||||
|
) {
|
||||||
|
val databaseName = mDatabase?.name
|
||||||
|
val accountName =
|
||||||
|
if (databaseName?.isBlank() != false)
|
||||||
|
getString(R.string.passkey_database_username)
|
||||||
|
else databaseName
|
||||||
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
||||||
val relyingPartyId = PublicKeyCredentialCreationOptions(
|
val relyingPartyId = PublicKeyCredentialCreationOptions(
|
||||||
requestJson = request.requestJson,
|
requestJson = request.requestJson,
|
||||||
@@ -309,6 +326,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
callback(createEntries)
|
||||||
},
|
},
|
||||||
onItemNotFound = { database ->
|
onItemNotFound = { database ->
|
||||||
// To create a new entry
|
// To create a new entry
|
||||||
@@ -317,6 +335,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
} else {
|
} else {
|
||||||
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||||
}
|
}
|
||||||
|
callback(createEntries)
|
||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
// Launch the passkey launcher activity to open the database
|
// Launch the passkey launcher activity to open the database
|
||||||
@@ -334,10 +353,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
callback(createEntries)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return BeginCreateCredentialResponse(createEntries)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClearCredentialStateRequest(
|
override fun onClearCredentialStateRequest(
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ParcelUuid
|
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -44,6 +43,7 @@ import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
|||||||
import com.kunzisoft.encrypt.Signature
|
import com.kunzisoft.encrypt.Signature
|
||||||
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
|
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
|
||||||
@@ -60,7 +60,6 @@ import com.kunzisoft.keepass.model.AndroidOrigin
|
|||||||
import com.kunzisoft.keepass.model.AppOrigin
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.Passkey
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.utils.AppUtil
|
import com.kunzisoft.keepass.utils.AppUtil
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
@@ -88,10 +87,7 @@ object PasskeyHelper {
|
|||||||
|
|
||||||
private const val HMAC_TYPE = "HmacSHA256"
|
private const val HMAC_TYPE = "HmacSHA256"
|
||||||
|
|
||||||
|
|
||||||
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
|
|
||||||
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
||||||
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
|
|
||||||
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
|
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
|
||||||
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
|
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
|
||||||
|
|
||||||
@@ -110,38 +106,6 @@ object PasskeyHelper {
|
|||||||
|
|
||||||
private val internalSecureRandom: SecureRandom = SecureRandom()
|
private val internalSecureRandom: SecureRandom = SecureRandom()
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the Passkey response for one entry
|
|
||||||
*/
|
|
||||||
fun Activity.buildPasskeyResponseAndSetResult(
|
|
||||||
entryInfo: EntryInfo,
|
|
||||||
extras: Bundle? = null
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
entryInfo.passkey?.let { passkey ->
|
|
||||||
val mReplyIntent = Intent()
|
|
||||||
Log.d(javaClass.name, "Success Passkey manual selection")
|
|
||||||
mReplyIntent.addPasskey(passkey)
|
|
||||||
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
|
|
||||||
mReplyIntent.addNodeId(entryInfo.id)
|
|
||||||
extras?.let {
|
|
||||||
mReplyIntent.putExtras(it)
|
|
||||||
}
|
|
||||||
setResult(Activity.RESULT_OK, mReplyIntent)
|
|
||||||
} ?: run {
|
|
||||||
throw IOException("No passkey found")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(javaClass.name, "Unable to add the passkey as result", e)
|
|
||||||
Toast.makeText(
|
|
||||||
this,
|
|
||||||
getString(R.string.error_passkey_result),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an authentication code generated by an entry to the intent
|
* Add an authentication code generated by an entry to the intent
|
||||||
*/
|
*/
|
||||||
@@ -181,22 +145,6 @@ object PasskeyHelper {
|
|||||||
return this.removeExtra(EXTRA_PASSKEY)
|
return this.removeExtra(EXTRA_PASSKEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the search info to the intent
|
|
||||||
*/
|
|
||||||
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
|
|
||||||
searchInfo?.let {
|
|
||||||
putExtra(EXTRA_SEARCH_INFO, searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the search info from the intent
|
|
||||||
*/
|
|
||||||
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
|
||||||
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the app origin to the intent
|
* Add the app origin to the intent
|
||||||
*/
|
*/
|
||||||
@@ -221,21 +169,37 @@ object PasskeyHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the node id to the intent, useful for auto passkey selection
|
* Build the Passkey response for one entry
|
||||||
*/
|
*/
|
||||||
fun Intent.addNodeId(nodeId: UUID?) {
|
fun Activity.buildPasskeyResponseAndSetResult(
|
||||||
nodeId?.let {
|
entryInfo: EntryInfo,
|
||||||
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
entryInfo.passkey?.let { passkey ->
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(javaClass.name, "Success Passkey manual selection")
|
||||||
|
mReplyIntent.addPasskey(passkey)
|
||||||
|
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
|
||||||
|
mReplyIntent.addNodeId(entryInfo.id)
|
||||||
|
extras?.let {
|
||||||
|
mReplyIntent.putExtras(it)
|
||||||
|
}
|
||||||
|
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
} ?: run {
|
||||||
|
throw IOException("No passkey found")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.name, "Unable to add the passkey as result", e)
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
getString(R.string.error_passkey_result),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the node id from the intent
|
|
||||||
*/
|
|
||||||
fun Intent.retrieveNodeId(): UUID? {
|
|
||||||
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the timestamp and authentication code transmitted via PendingIntent
|
* Check the timestamp and authentication code transmitted via PendingIntent
|
||||||
*/
|
*/
|
||||||
@@ -424,11 +388,15 @@ object PasskeyHelper {
|
|||||||
* Utility method to create a passkey and the associated creation request parameters
|
* Utility method to create a passkey and the associated creation request parameters
|
||||||
* [intent] allows to retrieve the request
|
* [intent] allows to retrieve the request
|
||||||
* [context] context to manage package verification files
|
* [context] context to manage package verification files
|
||||||
|
* [defaultBackupEligibility] the default backup eligibility to add the the passkey entry
|
||||||
|
* [defaultBackupState] the default backup state to add the the passkey entry
|
||||||
* [passkeyCreated] is called asynchronously when the passkey has been created
|
* [passkeyCreated] is called asynchronously when the passkey has been created
|
||||||
*/
|
*/
|
||||||
suspend fun retrievePasskeyCreationRequestParameters(
|
suspend fun retrievePasskeyCreationRequestParameters(
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
defaultBackupEligibility: Boolean?,
|
||||||
|
defaultBackupState: Boolean?,
|
||||||
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
|
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
|
||||||
) {
|
) {
|
||||||
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||||
@@ -456,7 +424,9 @@ object PasskeyHelper {
|
|||||||
privateKeyPem = privateKeyPem,
|
privateKeyPem = privateKeyPem,
|
||||||
credentialId = b64Encode(credentialId),
|
credentialId = b64Encode(credentialId),
|
||||||
userHandle = b64Encode(userHandle),
|
userHandle = b64Encode(userHandle),
|
||||||
relyingParty = relyingParty
|
relyingParty = relyingParty,
|
||||||
|
backupEligibility = defaultBackupEligibility,
|
||||||
|
backupState = defaultBackupState
|
||||||
)
|
)
|
||||||
|
|
||||||
// create new entry in database
|
// create new entry in database
|
||||||
@@ -590,8 +560,8 @@ object PasskeyHelper {
|
|||||||
requestOptions: PublicKeyCredentialRequestOptions,
|
requestOptions: PublicKeyCredentialRequestOptions,
|
||||||
clientDataResponse: ClientDataResponse,
|
clientDataResponse: ClientDataResponse,
|
||||||
passkey: Passkey,
|
passkey: Passkey,
|
||||||
backupEligibility: Boolean,
|
defaultBackupEligibility: Boolean,
|
||||||
backupState: Boolean
|
defaultBackupState: Boolean
|
||||||
): PublicKeyCredential {
|
): PublicKeyCredential {
|
||||||
val getCredentialResponse = FidoPublicKeyCredential(
|
val getCredentialResponse = FidoPublicKeyCredential(
|
||||||
id = passkey.credentialId,
|
id = passkey.credentialId,
|
||||||
@@ -599,8 +569,8 @@ object PasskeyHelper {
|
|||||||
requestOptions = requestOptions,
|
requestOptions = requestOptions,
|
||||||
userPresent = true,
|
userPresent = true,
|
||||||
userVerified = true,
|
userVerified = true,
|
||||||
backupEligibility = backupEligibility,
|
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
|
||||||
backupState = backupState,
|
backupState = passkey.backupState ?: defaultBackupState,
|
||||||
userHandle = passkey.userHandle,
|
userHandle = passkey.userHandle,
|
||||||
privateKey = passkey.privateKeyPem,
|
privateKey = passkey.privateKeyPem,
|
||||||
clientDataResponse = clientDataResponse
|
clientDataResponse = clientDataResponse
|
||||||
|
|||||||
@@ -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,151 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.viewmodel
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_CANCELED
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
abstract class CredentialLauncherViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
|
protected var mDatabase: ContextualDatabase? = null
|
||||||
|
|
||||||
|
protected var isResultLauncherRegistered: Boolean = false
|
||||||
|
private var mSelectionResult: ActivityResult? = null
|
||||||
|
|
||||||
|
protected val mCredentialUiState = MutableStateFlow<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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
|
mDatabase = database
|
||||||
|
mSelectionResult?.let { selectionResult ->
|
||||||
|
manageSelectionResult(database, selectionResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun manageSelectionResult(activityResult: ActivityResult) {
|
||||||
|
// Waiting for the database if needed
|
||||||
|
when (activityResult.resultCode) {
|
||||||
|
RESULT_OK -> {
|
||||||
|
mSelectionResult = activityResult
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
manageSelectionResult(database, activityResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RESULT_CANCELED -> {
|
||||||
|
cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun manageSelectionResult(database: ContextualDatabase, activityResult: ActivityResult) {
|
||||||
|
mSelectionResult = null
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun manageRegistrationResult(activityResult: ActivityResult) {}
|
||||||
|
|
||||||
|
open fun onExceptionOccurred(e: Throwable) {
|
||||||
|
showError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun launchActionIfNeeded(
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
database: ContextualDatabase?
|
||||||
|
) {
|
||||||
|
if (database != null) {
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
if (isResultLauncherRegistered.not()) {
|
||||||
|
isResultLauncherRegistered = true
|
||||||
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
onExceptionOccurred(e)
|
||||||
|
}) {
|
||||||
|
launchAction(intent, specialMode, database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the main action
|
||||||
|
*/
|
||||||
|
protected abstract suspend fun launchAction(
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
database: ContextualDatabase?
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class 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.GetCredentialResponse
|
||||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
import androidx.credentials.provider.PendingIntentHandler
|
import androidx.credentials.provider.PendingIntentHandler
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
@@ -25,11 +28,9 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVe
|
|||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -56,22 +57,21 @@ import java.io.InvalidObjectException
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
class PasskeyLauncherViewModel(application: Application): AndroidViewModel(application) {
|
class PasskeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
|
||||||
|
|
||||||
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
|
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
|
||||||
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
|
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
|
||||||
private var mPasskey: Passkey? = null
|
private var mPasskey: Passkey? = null
|
||||||
|
|
||||||
|
private var mLockDatabaseAfterSelection: Boolean = false
|
||||||
private var mBackupEligibility: Boolean = true
|
private var mBackupEligibility: Boolean = true
|
||||||
private var mBackupState: Boolean = false
|
private var mBackupState: Boolean = false
|
||||||
private var mLockDatabase: Boolean = true
|
|
||||||
|
|
||||||
private var isResultLauncherRegistered: Boolean = false
|
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||||
|
val uiState: StateFlow<UIState> = mUiState
|
||||||
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
|
|
||||||
val uiState: StateFlow<UIState> = _uiState
|
|
||||||
|
|
||||||
fun initialize() {
|
fun initialize() {
|
||||||
|
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
|
||||||
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
|
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
|
||||||
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
|
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
|
||||||
}
|
}
|
||||||
@@ -79,19 +79,14 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
fun showAppPrivilegedDialog(
|
fun showAppPrivilegedDialog(
|
||||||
temptingApp: AndroidPrivilegedApp
|
temptingApp: AndroidPrivilegedApp
|
||||||
) {
|
) {
|
||||||
_uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
|
mUiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showAppSignatureDialog(
|
fun showAppSignatureDialog(
|
||||||
temptingApp: AppOrigin,
|
temptingApp: AppOrigin,
|
||||||
nodeId: UUID
|
nodeId: UUID
|
||||||
) {
|
) {
|
||||||
_uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
|
mUiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
|
||||||
}
|
|
||||||
|
|
||||||
fun showError(error: Throwable) {
|
|
||||||
Log.e(TAG, "Error on passkey launch", error)
|
|
||||||
_uiState.value = UIState.ShowError(error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCustomPrivilegedApp(
|
fun saveCustomPrivilegedApp(
|
||||||
@@ -107,7 +102,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
context = getApplication(),
|
context = getApplication(),
|
||||||
privilegedApps = listOf(temptingApp)
|
privilegedApps = listOf(temptingApp)
|
||||||
)
|
)
|
||||||
launchPasskeyAction(
|
launchAction(
|
||||||
intent = intent,
|
intent = intent,
|
||||||
specialMode = specialMode,
|
specialMode = specialMode,
|
||||||
database = database
|
database = database
|
||||||
@@ -139,54 +134,33 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
)
|
)
|
||||||
entryInfo.saveAppOrigin(database, temptingApp)
|
entryInfo.saveAppOrigin(database, temptingApp)
|
||||||
newEntry.setEntryInfo(database, entryInfo)
|
newEntry.setEntryInfo(database, entryInfo)
|
||||||
_uiState.value = UIState.UpdateEntry(
|
mUiState.value = UIState.UpdateEntry(
|
||||||
oldEntry = entry,
|
oldEntry = entry,
|
||||||
newEntry = newEntry
|
newEntry = newEntry
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setResult(intent: Intent) {
|
override fun onExceptionOccurred(e: Throwable) {
|
||||||
// Remove the launcher register
|
if (e is PrivilegedAllowLists.PrivilegedException) {
|
||||||
isResultLauncherRegistered = false
|
showAppPrivilegedDialog(e.temptingApp)
|
||||||
_uiState.value = UIState.SetActivityResult(
|
} else {
|
||||||
lockDatabase = mLockDatabase,
|
super.onExceptionOccurred(e)
|
||||||
resultCode = RESULT_OK,
|
}
|
||||||
data = intent
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelResult() {
|
override fun launchActionIfNeeded(
|
||||||
isResultLauncherRegistered = false
|
|
||||||
_uiState.value = UIState.SetActivityResult(
|
|
||||||
lockDatabase = mLockDatabase,
|
|
||||||
resultCode = RESULT_CANCELED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchPasskeyActionIfNeeded(
|
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
specialMode: SpecialMode,
|
specialMode: SpecialMode,
|
||||||
database: ContextualDatabase?
|
database: ContextualDatabase?
|
||||||
) {
|
) {
|
||||||
if (isResultLauncherRegistered.not()) {
|
// Launch with database when a nodeId is present
|
||||||
isResultLauncherRegistered = true
|
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
|
||||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
super.launchActionIfNeeded(intent, specialMode, database)
|
||||||
if (e is PrivilegedAllowLists.PrivilegedException) {
|
|
||||||
showAppPrivilegedDialog(e.temptingApp)
|
|
||||||
} else {
|
|
||||||
showError(e)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
launchPasskeyAction(intent, specialMode, database)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override suspend fun launchAction(
|
||||||
* Launch the main action to manage Passkey
|
|
||||||
*/
|
|
||||||
private suspend fun launchPasskeyAction(
|
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
specialMode: SpecialMode,
|
specialMode: SpecialMode,
|
||||||
database: ContextualDatabase?
|
database: ContextualDatabase?
|
||||||
@@ -194,6 +168,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
|
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
|
||||||
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
|
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
|
||||||
val nodeId = intent.retrieveNodeId()
|
val nodeId = intent.retrieveNodeId()
|
||||||
|
intent.removeInfo()
|
||||||
|
intent.removeAppOrigin()
|
||||||
|
intent.removeNodeId()
|
||||||
checkSecurity(intent, nodeId)
|
checkSecurity(intent, nodeId)
|
||||||
when (specialMode) {
|
when (specialMode) {
|
||||||
SpecialMode.SELECTION -> {
|
SpecialMode.SELECTION -> {
|
||||||
@@ -260,15 +237,19 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
TAG, "No Passkey found for selection," +
|
TAG, "No Passkey found for selection," +
|
||||||
"launch manual selection in opened database"
|
"launch manual selection in opened database"
|
||||||
)
|
)
|
||||||
_uiState.value = UIState.LaunchGroupActivityForSelection(
|
mCredentialUiState.value =
|
||||||
database = openedDatabase
|
CredentialState.LaunchGroupActivityForSelection(
|
||||||
)
|
database = openedDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
Log.d(TAG, "Manual passkey selection in closed database")
|
Log.d(TAG, "Manual passkey selection in closed database")
|
||||||
_uiState.value =
|
mCredentialUiState.value =
|
||||||
UIState.LaunchFileDatabaseSelectActivityForSelection(
|
CredentialState.LaunchFileDatabaseSelectActivityForSelection(
|
||||||
searchInfo = searchInfo
|
searchInfo = searchInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -326,12 +307,12 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
appOrigin = appOrigin
|
appOrigin = appOrigin
|
||||||
),
|
),
|
||||||
passkey = passkey,
|
passkey = passkey,
|
||||||
backupEligibility = mBackupEligibility,
|
defaultBackupEligibility = mBackupEligibility,
|
||||||
backupState = mBackupState
|
defaultBackupState = mBackupState
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
setResult(result)
|
setResult(result, lockDatabase = mLockDatabaseAfterSelection)
|
||||||
} catch (e: SignatureNotFoundException) {
|
} catch (e: SignatureNotFoundException) {
|
||||||
// Request the dialog if signature exception
|
// Request the dialog if signature exception
|
||||||
showAppSignatureDialog(e.temptingApp, nodeId)
|
showAppSignatureDialog(e.temptingApp, nodeId)
|
||||||
@@ -340,9 +321,11 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun manageSelectionResult(
|
override fun manageSelectionResult(
|
||||||
|
database: ContextualDatabase,
|
||||||
activityResult: ActivityResult
|
activityResult: ActivityResult
|
||||||
) {
|
) {
|
||||||
|
super.manageSelectionResult(database, activityResult)
|
||||||
val intent = activityResult.data
|
val intent = activityResult.data
|
||||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
Log.e(TAG, "Unable to create selection response for passkey", e)
|
Log.e(TAG, "Unable to create selection response for passkey", e)
|
||||||
@@ -380,8 +363,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
appOrigin = appOrigin
|
appOrigin = appOrigin
|
||||||
),
|
),
|
||||||
passkey = passkey,
|
passkey = passkey,
|
||||||
backupEligibility = mBackupEligibility,
|
defaultBackupEligibility = mBackupEligibility,
|
||||||
backupState = mBackupState
|
defaultBackupState = mBackupState
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -389,7 +372,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
throw IOException("Usage parameters is null")
|
throw IOException("Usage parameters is null")
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
setResult(responseIntent)
|
setResult(responseIntent, lockDatabase = mLockDatabaseAfterSelection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,6 +400,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
retrievePasskeyCreationRequestParameters(
|
retrievePasskeyCreationRequestParameters(
|
||||||
intent = intent,
|
intent = intent,
|
||||||
context = getApplication(),
|
context = getApplication(),
|
||||||
|
defaultBackupEligibility = mBackupEligibility,
|
||||||
|
defaultBackupState = mBackupState,
|
||||||
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
|
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
|
||||||
// Save the requested parameters
|
// Save the requested parameters
|
||||||
mPasskey = passkey
|
mPasskey = passkey
|
||||||
@@ -440,24 +425,26 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
TAG, "Passkey found for registration, " +
|
TAG, "Passkey found for registration, " +
|
||||||
"but launch manual registration for a new entry"
|
"but launch manual registration for a new entry"
|
||||||
)
|
)
|
||||||
_uiState.value = UIState.LaunchGroupActivityForRegistration(
|
mCredentialUiState.value =
|
||||||
database = openedDatabase,
|
CredentialState.LaunchGroupActivityForRegistration(
|
||||||
registerInfo = registerInfo,
|
database = openedDatabase,
|
||||||
typeMode = TypeMode.PASSKEY
|
registerInfo = registerInfo,
|
||||||
)
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onItemNotFound = { openedDatabase ->
|
onItemNotFound = { openedDatabase ->
|
||||||
Log.d(TAG, "Launch new manual registration in opened database")
|
Log.d(TAG, "Launch new manual registration in opened database")
|
||||||
_uiState.value = UIState.LaunchGroupActivityForRegistration(
|
mCredentialUiState.value =
|
||||||
database = openedDatabase,
|
CredentialState.LaunchGroupActivityForRegistration(
|
||||||
registerInfo = registerInfo,
|
database = openedDatabase,
|
||||||
typeMode = TypeMode.PASSKEY
|
registerInfo = registerInfo,
|
||||||
)
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
Log.d(TAG, "Manual passkey registration in closed database")
|
Log.d(TAG, "Manual passkey registration in closed database")
|
||||||
_uiState.value =
|
mCredentialUiState.value =
|
||||||
UIState.LaunchFileDatabaseSelectActivityForRegistration(
|
CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
|
||||||
registerInfo = registerInfo,
|
registerInfo = registerInfo,
|
||||||
typeMode = TypeMode.PASSKEY
|
typeMode = TypeMode.PASSKEY
|
||||||
)
|
)
|
||||||
@@ -490,7 +477,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun manageRegistrationResult(activityResult: ActivityResult) {
|
override fun manageRegistrationResult(activityResult: ActivityResult) {
|
||||||
val intent = activityResult.data
|
val intent = activityResult.data
|
||||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
Log.e(TAG, "Unable to create registration response for passkey", e)
|
Log.e(TAG, "Unable to create registration response for passkey", e)
|
||||||
@@ -518,8 +505,10 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
intent = responseIntent,
|
intent = responseIntent,
|
||||||
response = buildCreatePublicKeyCredentialResponse(
|
response = buildCreatePublicKeyCredentialResponse(
|
||||||
publicKeyCredentialCreationParameters = it,
|
publicKeyCredentialCreationParameters = it,
|
||||||
backupEligibility = mBackupEligibility,
|
backupEligibility = passkey?.backupEligibility
|
||||||
backupState = mBackupState
|
?: mBackupEligibility,
|
||||||
|
backupState = passkey?.backupState
|
||||||
|
?: mBackupState
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -549,29 +538,6 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
val temptingApp: AppOrigin,
|
val temptingApp: AppOrigin,
|
||||||
val nodeId: UUID
|
val nodeId: UUID
|
||||||
): UIState()
|
): UIState()
|
||||||
data class LaunchGroupActivityForSelection(
|
|
||||||
val database: ContextualDatabase
|
|
||||||
): UIState()
|
|
||||||
data class LaunchGroupActivityForRegistration(
|
|
||||||
val database: ContextualDatabase,
|
|
||||||
val registerInfo: RegisterInfo,
|
|
||||||
val typeMode: TypeMode
|
|
||||||
): UIState()
|
|
||||||
data class LaunchFileDatabaseSelectActivityForSelection(
|
|
||||||
val searchInfo: SearchInfo
|
|
||||||
): UIState()
|
|
||||||
data class LaunchFileDatabaseSelectActivityForRegistration(
|
|
||||||
val registerInfo: RegisterInfo,
|
|
||||||
val typeMode: TypeMode
|
|
||||||
): UIState()
|
|
||||||
data class SetActivityResult(
|
|
||||||
val lockDatabase: Boolean,
|
|
||||||
val resultCode: Int,
|
|
||||||
val data: Intent? = null
|
|
||||||
): UIState()
|
|
||||||
data class ShowError(
|
|
||||||
val error: Throwable
|
|
||||||
): UIState()
|
|
||||||
data class UpdateEntry(
|
data class UpdateEntry(
|
||||||
val oldEntry: Entry,
|
val oldEntry: Entry,
|
||||||
val newEntry: Entry
|
val newEntry: Entry
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database
|
package com.kunzisoft.keepass.database
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -29,23 +28,15 @@ import android.content.Context.BIND_IMPORTANT
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
|
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
@@ -55,7 +46,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
|
||||||
@@ -89,13 +79,9 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.putParcelableList
|
import com.kunzisoft.keepass.utils.putParcelableList
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,121 +89,29 @@ import java.util.UUID
|
|||||||
* Useful to retrieve a database instance and sending tasks commands
|
* Useful to retrieve a database instance and sending tasks commands
|
||||||
*/
|
*/
|
||||||
class DatabaseTaskProvider(
|
class DatabaseTaskProvider(
|
||||||
private var context: Context,
|
private var context: Context
|
||||||
private var showDialog: Boolean = true
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// To show dialog only if context is an activity
|
|
||||||
private var activity: FragmentActivity? = try {
|
|
||||||
context as? FragmentActivity?
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
||||||
|
|
||||||
var onActionFinish: ((
|
var onStartActionRequested: ((bundle: Bundle?, actionTask: String) -> Unit)? = null
|
||||||
database: ContextualDatabase,
|
var actionTaskListener: DatabaseTaskNotificationService.ActionTaskListener? = null
|
||||||
actionTask: String,
|
var databaseInfoListener: DatabaseTaskNotificationService.DatabaseInfoListener? = null
|
||||||
result: ActionRunnable.Result
|
|
||||||
) -> Unit)? = null
|
|
||||||
|
|
||||||
private var intentDatabaseTask: Intent = Intent(
|
|
||||||
context.applicationContext,
|
|
||||||
DatabaseTaskNotificationService::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||||
|
|
||||||
private var serviceConnection: ServiceConnection? = null
|
private var serviceConnection: ServiceConnection? = null
|
||||||
|
|
||||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
|
||||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
this.activity = null
|
|
||||||
this.onDatabaseRetrieved = null
|
this.onDatabaseRetrieved = null
|
||||||
this.onActionFinish = null
|
|
||||||
this.databaseTaskBroadcastReceiver = null
|
this.databaseTaskBroadcastReceiver = null
|
||||||
this.mBinder = null
|
this.mBinder = null
|
||||||
this.serviceConnection = null
|
this.serviceConnection = null
|
||||||
this.progressTaskDialogFragment = null
|
|
||||||
this.databaseChangedDialogFragment = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
|
fun onDatabaseChangeValidated() {
|
||||||
override fun onActionStarted(
|
mBinder?.getService()?.saveDatabaseInfo()
|
||||||
database: ContextualDatabase,
|
|
||||||
progressMessage: ProgressMessage
|
|
||||||
) {
|
|
||||||
if (showDialog)
|
|
||||||
startDialog(progressMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionUpdated(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
progressMessage: ProgressMessage
|
|
||||||
) {
|
|
||||||
if (showDialog)
|
|
||||||
updateDialog(progressMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionStopped(
|
|
||||||
database: ContextualDatabase
|
|
||||||
) {
|
|
||||||
// Remove the progress task
|
|
||||||
stopDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionFinished(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
actionTask: String,
|
|
||||||
result: ActionRunnable.Result
|
|
||||||
) {
|
|
||||||
onActionFinish?.invoke(database, actionTask, result)
|
|
||||||
onActionStopped(database)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mActionDatabaseListener =
|
|
||||||
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
|
||||||
override fun validateDatabaseChanged() {
|
|
||||||
mBinder?.getService()?.saveDatabaseInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var databaseInfoListener = object :
|
|
||||||
DatabaseTaskNotificationService.DatabaseInfoListener {
|
|
||||||
override fun onDatabaseInfoChanged(
|
|
||||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
|
||||||
newDatabaseInfo: SnapFileDatabaseInfo,
|
|
||||||
readOnlyDatabase: Boolean
|
|
||||||
) {
|
|
||||||
activity?.let { activity ->
|
|
||||||
activity.lifecycleScope.launch {
|
|
||||||
if (databaseChangedDialogFragment == null) {
|
|
||||||
databaseChangedDialogFragment = activity.supportFragmentManager
|
|
||||||
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
|
||||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
|
||||||
mActionDatabaseListener
|
|
||||||
}
|
|
||||||
if (progressTaskDialogFragment == null) {
|
|
||||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
|
||||||
previousDatabaseInfo,
|
|
||||||
newDatabaseInfo,
|
|
||||||
readOnlyDatabase
|
|
||||||
)
|
|
||||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
|
||||||
mActionDatabaseListener
|
|
||||||
databaseChangedDialogFragment?.show(
|
|
||||||
activity.supportFragmentManager,
|
|
||||||
DATABASE_CHANGED_DIALOG_TAG
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
|
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
|
||||||
@@ -226,48 +120,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() {
|
private fun initServiceConnection() {
|
||||||
if (serviceConnection == null) {
|
if (serviceConnection == null) {
|
||||||
serviceConnection = object : ServiceConnection {
|
serviceConnection = object : ServiceConnection {
|
||||||
override fun onBindingDied(name: ComponentName?) {
|
override fun onBindingDied(name: ComponentName?) {
|
||||||
stopDialog()
|
actionTaskListener?.onActionStopped()
|
||||||
|
onDatabaseRetrieved?.invoke(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNullBinding(name: ComponentName?) {
|
override fun onNullBinding(name: ComponentName?) {
|
||||||
stopDialog()
|
actionTaskListener?.onActionStopped()
|
||||||
|
onDatabaseRetrieved?.invoke(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
@@ -290,21 +153,33 @@ class DatabaseTaskProvider(
|
|||||||
|
|
||||||
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||||
service?.addDatabaseListener(databaseListener)
|
service?.addDatabaseListener(databaseListener)
|
||||||
service?.addDatabaseFileInfoListener(databaseInfoListener)
|
databaseInfoListener?.let { infoListener ->
|
||||||
service?.addActionTaskListener(actionTaskListener)
|
service?.addDatabaseFileInfoListener(infoListener)
|
||||||
|
}
|
||||||
|
actionTaskListener?.let { taskListener ->
|
||||||
|
service?.addActionTaskListener(taskListener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||||
service?.removeActionTaskListener(actionTaskListener)
|
actionTaskListener?.let { taskListener ->
|
||||||
service?.removeDatabaseFileInfoListener(databaseInfoListener)
|
service?.removeActionTaskListener(taskListener)
|
||||||
|
}
|
||||||
|
databaseInfoListener?.let { infoListener ->
|
||||||
|
service?.removeDatabaseFileInfoListener(infoListener)
|
||||||
|
}
|
||||||
service?.removeDatabaseListener(databaseListener)
|
service?.removeDatabaseListener(databaseListener)
|
||||||
|
onDatabaseRetrieved?.invoke(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
initServiceConnection()
|
initServiceConnection()
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
context.bindService(
|
context.bindService(
|
||||||
intentDatabaseTask,
|
Intent(
|
||||||
|
context.applicationContext,
|
||||||
|
DatabaseTaskNotificationService::class.java
|
||||||
|
),
|
||||||
it,
|
it,
|
||||||
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
|
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
|
||||||
)
|
)
|
||||||
@@ -368,58 +243,9 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
|
|
||||||
private val requestPermissionLauncher = activity?.registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { _ ->
|
|
||||||
// Whether or not the user has accepted, the service can be started,
|
|
||||||
// There just won't be any notification if it's not allowed.
|
|
||||||
tempServiceParameters.removeFirstOrNull()?.let {
|
|
||||||
startService(it.first, it.second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun start(bundle: Bundle? = null, actionTask: String) {
|
private fun start(bundle: Bundle? = null, actionTask: String) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
onStartActionRequested?.invoke(bundle, actionTask) ?: run {
|
||||||
val contextActivity = activity
|
context.startDatabaseService(bundle, actionTask)
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
== PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
startService(bundle, actionTask)
|
|
||||||
} else if (contextActivity != null && shouldShowRequestPermissionRationale(
|
|
||||||
contextActivity,
|
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// it's not the first time, so the user deliberately chooses not to display the notification
|
|
||||||
startService(bundle, actionTask)
|
|
||||||
} else {
|
|
||||||
AlertDialog.Builder(context)
|
|
||||||
.setMessage(R.string.warning_database_notification_permission)
|
|
||||||
.setNegativeButton(R.string.later) { _, _ ->
|
|
||||||
// Refuses the notification, so start the service
|
|
||||||
startService(bundle, actionTask)
|
|
||||||
}
|
|
||||||
.setPositiveButton(R.string.ask) { _, _ ->
|
|
||||||
// Save the temp parameters to ask the permission
|
|
||||||
tempServiceParameters.add(Pair(bundle, actionTask))
|
|
||||||
requestPermissionLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
}.create().show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startService(bundle, actionTask)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startService(bundle: Bundle? = null, actionTask: String) {
|
|
||||||
try {
|
|
||||||
if (bundle != null)
|
|
||||||
intentDatabaseTask.putExtras(bundle)
|
|
||||||
intentDatabaseTask.action = actionTask
|
|
||||||
context.startService(intentDatabaseTask)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to perform database action", e)
|
|
||||||
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,5 +668,21 @@ class DatabaseTaskProvider(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = DatabaseTaskProvider::class.java.name
|
private val TAG = DatabaseTaskProvider::class.java.name
|
||||||
|
|
||||||
|
fun Context.startDatabaseService(bundle: Bundle? = null, actionTask: String) {
|
||||||
|
try {
|
||||||
|
val intentDatabaseTask = Intent(
|
||||||
|
applicationContext,
|
||||||
|
DatabaseTaskNotificationService::class.java
|
||||||
|
)
|
||||||
|
if (bundle != null)
|
||||||
|
intentDatabaseTask.putExtras(bundle)
|
||||||
|
intentDatabaseTask.action = actionTask
|
||||||
|
startService(intentDatabaseTask)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to perform database action", e)
|
||||||
|
Toast.makeText(this, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.StringRes
|
|||||||
|
|
||||||
data class ProgressMessage(
|
data class ProgressMessage(
|
||||||
@StringRes
|
@StringRes
|
||||||
var titleId: Int,
|
var titleId: Int? = null,
|
||||||
@StringRes
|
@StringRes
|
||||||
var messageId: Int? = null,
|
var messageId: Int? = null,
|
||||||
@StringRes
|
@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.VersionDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
|
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
|
||||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BE
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BS
|
||||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
|
||||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
|
||||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
|
||||||
@@ -146,6 +148,8 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
|||||||
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
|
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
|
||||||
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
|
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
|
||||||
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
|
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
|
||||||
|
FIELD_FLAG_BE.equals(name, true) -> context.getString(R.string.passkey_backup_eligibility)
|
||||||
|
FIELD_FLAG_BS.equals(name, true) -> context.getString(R.string.passkey_backup_state)
|
||||||
|
|
||||||
else -> name
|
else -> name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,16 @@ package com.kunzisoft.keepass.database.helper
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
|
|
||||||
object SearchHelper {
|
object SearchHelper {
|
||||||
|
|
||||||
@@ -40,6 +47,76 @@ object SearchHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the concrete web domain AKA without sub domain if needed
|
||||||
|
*/
|
||||||
|
private fun getConcreteWebDomain(
|
||||||
|
context: Context,
|
||||||
|
webDomain: String?,
|
||||||
|
concreteWebDomain: (searchSubDomains: Boolean, concreteWebDomain: String?) -> Unit
|
||||||
|
) {
|
||||||
|
val domain = webDomain
|
||||||
|
val searchSubDomains = searchSubDomains(context)
|
||||||
|
if (domain != null) {
|
||||||
|
// Warning, web domain can contains IP, don't crop in this case
|
||||||
|
if (searchSubDomains
|
||||||
|
|| Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
|
||||||
|
concreteWebDomain.invoke(searchSubDomains, webDomain)
|
||||||
|
} else {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val publicSuffixList = PublicSuffixList(context)
|
||||||
|
val publicSuffix = publicSuffixList
|
||||||
|
.getPublicSuffixPlusOne(domain).await()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
concreteWebDomain.invoke(false, publicSuffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
concreteWebDomain.invoke(searchSubDomains, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create search parameters asynchronously from [SearchInfo]
|
||||||
|
*/
|
||||||
|
fun SearchInfo.getSearchParametersFromSearchInfo(
|
||||||
|
context: Context,
|
||||||
|
callback: (SearchParameters) -> Unit
|
||||||
|
) {
|
||||||
|
getConcreteWebDomain(
|
||||||
|
context,
|
||||||
|
webDomain
|
||||||
|
) { searchSubDomains, concreteDomain ->
|
||||||
|
var query = this.toString()
|
||||||
|
if (isDomainSearch && concreteDomain != null)
|
||||||
|
query = concreteDomain
|
||||||
|
callback.invoke(
|
||||||
|
SearchParameters().apply {
|
||||||
|
searchQuery = query
|
||||||
|
allowEmptyQuery = false
|
||||||
|
searchInTitles = false
|
||||||
|
searchInUsernames = false
|
||||||
|
searchInPasswords = false
|
||||||
|
searchInAppIds = isAppIdSearch
|
||||||
|
searchInUrls = isDomainSearch
|
||||||
|
searchByDomain = true
|
||||||
|
searchBySubDomain = searchSubDomains
|
||||||
|
searchInRelyingParty = isPasskeySearch
|
||||||
|
searchInNotes = false
|
||||||
|
searchInOTP = isOTPSearch
|
||||||
|
searchInOther = false
|
||||||
|
searchInUUIDs = false
|
||||||
|
searchInTags = isTagSearch
|
||||||
|
searchInCurrentGroup = false
|
||||||
|
searchInSearchableGroup = true
|
||||||
|
searchInRecycleBin = false
|
||||||
|
searchInTemplates = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||||
*/
|
*/
|
||||||
@@ -52,28 +129,31 @@ object SearchHelper {
|
|||||||
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
||||||
onDatabaseClosed: () -> Unit
|
onDatabaseClosed: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
// Do not place coroutine at start, bug in Passkey implementation
|
||||||
if (database == null || !database.loaded) {
|
if (database == null || !database.loaded) {
|
||||||
onDatabaseClosed.invoke()
|
onDatabaseClosed.invoke()
|
||||||
} else if (TimeoutHelper.checkTime(context)) {
|
} else if (TimeoutHelper.checkTime(context)) {
|
||||||
var searchWithoutUI = false
|
|
||||||
if (searchInfo != null
|
if (searchInfo != null
|
||||||
&& !searchInfo.manualSelection
|
&& !searchInfo.manualSelection
|
||||||
&& !searchInfo.containsOnlyNullValues()) {
|
&& !searchInfo.containsOnlyNullValues()
|
||||||
// If search provide results
|
) {
|
||||||
database.createVirtualGroupFromSearchInfo(
|
searchInfo.getSearchParametersFromSearchInfo(context) { searchParameters ->
|
||||||
searchInfo,
|
// If search provide results
|
||||||
MAX_SEARCH_ENTRY
|
database.createVirtualGroupFromSearchInfo(
|
||||||
)?.let { searchGroup ->
|
searchParameters = searchParameters,
|
||||||
if (searchGroup.numberOfChildEntries > 0) {
|
max = MAX_SEARCH_ENTRY
|
||||||
searchWithoutUI = true
|
)?.let { searchGroup ->
|
||||||
onItemsFound.invoke(database,
|
if (searchGroup.numberOfChildEntries > 0) {
|
||||||
searchGroup.getChildEntriesInfo(database))
|
onItemsFound.invoke(
|
||||||
}
|
database,
|
||||||
|
searchGroup.getChildEntriesInfo(database)
|
||||||
|
)
|
||||||
|
} else
|
||||||
|
onItemNotFound.invoke(database)
|
||||||
|
} ?: onItemNotFound.invoke(database)
|
||||||
}
|
}
|
||||||
}
|
} else
|
||||||
if (!searchWithoutUI) {
|
|
||||||
onItemNotFound.invoke(database)
|
onItemNotFound.invoke(database)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,20 +33,22 @@ import java.util.*
|
|||||||
class PasswordGenerator(private val resources: Resources) {
|
class PasswordGenerator(private val resources: Resources) {
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
fun generatePassword(length: Int,
|
fun generatePassword(
|
||||||
upperCase: Boolean,
|
length: Int,
|
||||||
lowerCase: Boolean,
|
upperCase: Boolean,
|
||||||
digits: Boolean,
|
lowerCase: Boolean,
|
||||||
minus: Boolean,
|
digits: Boolean,
|
||||||
underline: Boolean,
|
minus: Boolean,
|
||||||
space: Boolean,
|
underline: Boolean,
|
||||||
specials: Boolean,
|
space: Boolean,
|
||||||
brackets: Boolean,
|
specials: Boolean,
|
||||||
extended: Boolean,
|
brackets: Boolean,
|
||||||
considerChars: String,
|
extended: Boolean,
|
||||||
ignoreChars: String,
|
considerChars: String,
|
||||||
atLeastOneFromEach: Boolean,
|
ignoreChars: String,
|
||||||
excludeAmbiguousChar: Boolean): String {
|
atLeastOneFromEach: Boolean,
|
||||||
|
excludeAmbiguousChar: Boolean
|
||||||
|
): String {
|
||||||
// Desired password length is 0 or less
|
// Desired password length is 0 or less
|
||||||
if (length <= 0) {
|
if (length <= 0) {
|
||||||
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
|
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
|
||||||
@@ -228,7 +230,7 @@ class PasswordGenerator(private val resources: Resources) {
|
|||||||
private const val MINUS_CHAR = "-"
|
private const val MINUS_CHAR = "-"
|
||||||
private const val UNDERLINE_CHAR = "_"
|
private const val UNDERLINE_CHAR = "_"
|
||||||
private const val SPACE_CHAR = " "
|
private const val SPACE_CHAR = " "
|
||||||
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
|
private const val SPECIAL_CHARS = "&/,^@.#:%\\='$!?*`;+\"|~"
|
||||||
private const val BRACKET_CHARS = "[]{}()<>"
|
private const val BRACKET_CHARS = "[]{}()<>"
|
||||||
private const val AMBIGUOUS_CHARS = "iI|lLoO01"
|
private const val AMBIGUOUS_CHARS = "iI|lLoO01"
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.model.AttachmentState
|
|||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -164,7 +165,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (attachmentNotificationList.isEmpty()) {
|
if (attachmentNotificationList.isEmpty()) {
|
||||||
stopSelf()
|
stopService()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +195,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
||||||
|
|
||||||
val pendingContentIntent = PendingIntent.getActivity(this,
|
val pendingContentIntent = PendingIntent.getActivity(this,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
setDataAndType(attachmentNotification.uri,
|
setDataAndType(attachmentNotification.uri,
|
||||||
@@ -208,7 +209,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val pendingDeleteIntent = PendingIntent.getService(this,
|
val pendingDeleteIntent = PendingIntent.getService(this,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
||||||
// No action to delete the service
|
// No action to delete the service
|
||||||
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
sendBroadcast(Intent(LOCK_ACTION))
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
}
|
}
|
||||||
// Stop the service
|
// Stop the service
|
||||||
stopSelf()
|
stopService()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
|||||||
@@ -61,13 +61,14 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
@@ -175,7 +176,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
progressMessage: ProgressMessage
|
progressMessage: ProgressMessage
|
||||||
)
|
)
|
||||||
fun onActionStopped(
|
fun onActionStopped(
|
||||||
database: ContextualDatabase
|
database: ContextualDatabase? = null
|
||||||
)
|
)
|
||||||
fun onActionFinished(
|
fun onActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
@@ -261,11 +262,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
/* Do not stopped here, service cannot be connected
|
||||||
mActionTaskListeners.forEach { actionTaskListener ->
|
mActionTaskListeners.forEach { actionTaskListener ->
|
||||||
actionTaskListener.onActionStopped(
|
actionTaskListener.onActionStopped(
|
||||||
database
|
database
|
||||||
)
|
)
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,7 +339,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
val intentAction = intent?.action
|
val intentAction = intent?.action
|
||||||
|
|
||||||
if (intentAction == null && !database.loaded) {
|
if (intentAction == null && !database.loaded) {
|
||||||
stopSelf()
|
stopService()
|
||||||
}
|
}
|
||||||
|
|
||||||
val actionRunnable: ActionRunnable? = when (intentAction) {
|
val actionRunnable: ActionRunnable? = when (intentAction) {
|
||||||
@@ -446,10 +448,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||||
// Stop service after save if user remove task
|
// Stop service after save if user remove task
|
||||||
if (save && mTaskRemovedRequested) {
|
if (save && mTaskRemovedRequested) {
|
||||||
actionOnLock()
|
stopService()
|
||||||
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
|
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
|
||||||
if (!database.loaded) {
|
if (!database.loaded) {
|
||||||
stopSelf()
|
stopService()
|
||||||
} else {
|
} else {
|
||||||
// Restart the service to open lock notification
|
// Restart the service to open lock notification
|
||||||
try {
|
try {
|
||||||
@@ -534,11 +536,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
|
|
||||||
val notificationBuilder = buildNewNotification().apply {
|
val notificationBuilder = buildNewNotification().apply {
|
||||||
setSmallIcon(iconId)
|
setSmallIcon(iconId)
|
||||||
intent?.let {
|
val titleId = mProgressMessage.titleId?.let {
|
||||||
setContentTitle(getString(
|
intent?.getIntExtra(DATABASE_TASK_TITLE_KEY, it)
|
||||||
intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId))
|
} ?: R.string.app_name
|
||||||
)
|
setContentTitle(getString(titleId))
|
||||||
}
|
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setContentIntent(null)
|
setContentIntent(null)
|
||||||
}
|
}
|
||||||
@@ -550,7 +551,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
// Build Intents for notification action
|
// Build Intents for notification action
|
||||||
val pendingDatabaseIntent = PendingIntent.getActivity(
|
val pendingDatabaseIntent = PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent(this, GroupActivity::class.java),
|
Intent(this, GroupActivity::class.java),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
@@ -672,13 +673,19 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
updateMessage(R.string.decrypting_db)
|
updateMessage(R.string.decrypting_db)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun actionOnLock() {
|
override fun stopService() {
|
||||||
if (!TimeoutHelper.temporarilyDisableLock) {
|
if (!TimeoutHelper.temporarilyDisableLock) {
|
||||||
closeDatabase(mDatabase)
|
closeDatabase(mDatabase)
|
||||||
|
// Remove the database during the lock
|
||||||
|
// And notify each subscriber
|
||||||
|
mDatabase = null
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onDatabaseRetrieved(null)
|
||||||
|
}
|
||||||
// Remove the lock timer (no more needed if it exists)
|
// Remove the lock timer (no more needed if it exists)
|
||||||
TimeoutHelper.cancelLockTimer(this)
|
TimeoutHelper.cancelLockTimer(this)
|
||||||
// Service is stopped after receive the broadcast
|
// Service is stopped after receive the broadcast
|
||||||
super.actionOnLock()
|
super.stopService()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,9 +716,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
notifyProgressMessage()
|
notifyProgressMessage()
|
||||||
HardwareKeyActivity
|
HardwareKeyActivity
|
||||||
.launchHardwareKeyActivity(
|
.launchHardwareKeyActivity(
|
||||||
this@DatabaseTaskNotificationService,
|
context = this@DatabaseTaskNotificationService,
|
||||||
hardwareKey,
|
hardwareKey = hardwareKey,
|
||||||
seed
|
seed = seed
|
||||||
)
|
)
|
||||||
// Wait the response
|
// Wait the response
|
||||||
mProgressMessage.apply {
|
mProgressMessage.apply {
|
||||||
@@ -722,7 +729,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
// Close channels
|
// Close channels
|
||||||
closeChallengeResponse()
|
closeChallengeResponse()
|
||||||
// Restore previous message
|
// Restore previous message
|
||||||
mProgressMessage = previousMessage
|
mProgressMessage = previousMessage.apply {
|
||||||
|
cancelable = null
|
||||||
|
}
|
||||||
notifyProgressMessage()
|
notifyProgressMessage()
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
|||||||
sendBroadcast(Intent(LOCK_ACTION))
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
}
|
}
|
||||||
// Stop the service
|
// Stop the service
|
||||||
stopSelf()
|
stopService()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.services
|
package com.kunzisoft.keepass.services
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.core.app.ServiceCompat
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.LockReceiver
|
import com.kunzisoft.keepass.utils.LockReceiver
|
||||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||||
@@ -29,13 +28,7 @@ import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
|||||||
abstract class LockNotificationService : NotificationService() {
|
abstract class LockNotificationService : NotificationService() {
|
||||||
|
|
||||||
private var mLockReceiver: LockReceiver = LockReceiver {
|
private var mLockReceiver: LockReceiver = LockReceiver {
|
||||||
actionOnLock()
|
stopService()
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun actionOnLock() {
|
|
||||||
// Stop the service in all cases
|
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
|
||||||
stopSelf()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -46,7 +39,7 @@ abstract class LockNotificationService : NotificationService() {
|
|||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
if (!TimeoutHelper.temporarilyDisableLock) {
|
if (!TimeoutHelper.temporarilyDisableLock) {
|
||||||
actionOnLock()
|
stopService()
|
||||||
}
|
}
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import android.util.TypedValue
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
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,
|
protected fun defineTimerJob(builder: NotificationCompat.Builder,
|
||||||
type: NotificationServiceType,
|
type: NotificationServiceType,
|
||||||
timeoutMilliseconds: Long,
|
timeoutMilliseconds: Long,
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
autofillInlineSuggestionsPreference?.isVisible = false
|
autofillInlineSuggestionsPreference?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val autofillAskSaveDataPreference: TwoStatePreference? = findPreference(getString(R.string.autofill_ask_to_save_data_key))
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
autofillAskSaveDataPreference?.isVisible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ abstract class ExternalSettingsActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
private var lockView: FloatingActionButton? = null
|
private var lockView: FloatingActionButton? = null
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|||||||
@@ -21,20 +21,25 @@ package com.kunzisoft.keepass.settings
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
private var mCallback: Callback? = null
|
private var mCallback: Callback? = null
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
private var mDatabaseLoaded: Boolean = false
|
private val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
@@ -50,20 +55,24 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
mCallback = null
|
mCallback = null
|
||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
mDatabaseLoaded = database?.loaded == true
|
super.onCreate(savedInstanceState)
|
||||||
checkDatabaseLoaded()
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
checkDatabaseLoaded(database?.loaded == true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkDatabaseLoaded() {
|
private fun checkDatabaseLoaded(isDatabaseLoaded: Boolean) {
|
||||||
findPreference<Preference>(getString(R.string.settings_database_key))
|
findPreference<Preference>(getString(R.string.settings_database_key))
|
||||||
?.isEnabled = mDatabaseLoaded
|
?.isEnabled = isDatabaseLoaded
|
||||||
|
|
||||||
findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key))
|
findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key))
|
||||||
?.isVisible = mDatabaseLoaded
|
?.isVisible = isDatabaseLoaded
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
@@ -119,7 +128,7 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkDatabaseLoaded()
|
checkDatabaseLoaded(mDatabase?.loaded == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
|
|||||||
@@ -19,13 +19,21 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.settings
|
package com.kunzisoft.keepass.settings
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.TwoStatePreference
|
import androidx.preference.TwoStatePreference
|
||||||
@@ -39,19 +47,40 @@ import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
|||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.helper.*
|
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.settings.preference.*
|
import com.kunzisoft.keepass.settings.preference.DialogColorPreference
|
||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
|
import com.kunzisoft.keepass.settings.preference.DialogListExplanationPreference
|
||||||
|
import com.kunzisoft.keepass.settings.preference.InputKdfNumberPreference
|
||||||
|
import com.kunzisoft.keepass.settings.preference.InputKdfSizePreference
|
||||||
|
import com.kunzisoft.keepass.settings.preference.InputNumberPreference
|
||||||
|
import com.kunzisoft.keepass.settings.preference.InputTextPreference
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseColorPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDataCompressionPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDefaultUsernamePreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDescriptionPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistorySizePreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMemoryUsagePreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseNamePreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseParallelismPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRoundsPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseTemplatesGroupPreferenceDialogFragmentCompat
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.getSerializableCompat
|
import com.kunzisoft.keepass.utils.getSerializableCompat
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
|
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
private var mDatabase: ContextualDatabase? = null
|
private val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
private var mDatabaseReadOnly: Boolean = false
|
private var mDatabaseReadOnly: Boolean = false
|
||||||
private var mMergeDataAllowed: Boolean = false
|
private var mMergeDataAllowed: Boolean = false
|
||||||
private var mDatabaseAutoSaveEnabled: Boolean = true
|
private var mDatabaseAutoSaveEnabled: Boolean = true
|
||||||
@@ -114,19 +143,46 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
database?.let {
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
mDatabase = database
|
view.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||||
view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded)
|
context = requireContext(),
|
||||||
onDatabaseRetrieved(database)
|
databaseLoaded = database?.loaded
|
||||||
}
|
)
|
||||||
|
}
|
||||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
|
|
||||||
onDatabaseActionFinished(it.database, it.actionTask, it.result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,29 +223,26 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
mDatabaseViewModel.reloadDatabase(false)
|
mDatabaseViewModel.reloadDatabase(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
mDatabase = database
|
mDatabaseReadOnly = database.isReadOnly
|
||||||
mDatabaseReadOnly = database?.isReadOnly == true
|
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||||
mMergeDataAllowed = database?.isMergeDataAllowed() == true
|
|
||||||
|
|
||||||
mDatabase?.let {
|
if (database.loaded) {
|
||||||
if (it.loaded) {
|
when (mScreen) {
|
||||||
when (mScreen) {
|
Screen.DATABASE -> {
|
||||||
Screen.DATABASE -> {
|
onCreateDatabasePreference(database)
|
||||||
onCreateDatabasePreference(it)
|
}
|
||||||
}
|
Screen.DATABASE_SECURITY -> {
|
||||||
Screen.DATABASE_SECURITY -> {
|
onCreateDatabaseSecurityPreference(database)
|
||||||
onCreateDatabaseSecurityPreference(it)
|
}
|
||||||
}
|
Screen.DATABASE_MASTER_KEY -> {
|
||||||
Screen.DATABASE_MASTER_KEY -> {
|
onCreateDatabaseMasterKeyPreference(database)
|
||||||
onCreateDatabaseMasterKeyPreference(it)
|
}
|
||||||
}
|
else -> {
|
||||||
else -> {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.e(javaClass.name, "Database isn't ready")
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(javaClass.name, "Database isn't ready")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +511,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newDefaultUsername
|
newDefaultUsername
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.defaultUsername = oldDefaultUsername
|
database.defaultUsername = oldDefaultUsername
|
||||||
oldDefaultUsername
|
oldDefaultUsername
|
||||||
}
|
}
|
||||||
dbDefaultUsernamePref?.summary = defaultUsernameToShow
|
dbDefaultUsernamePref?.summary = defaultUsernameToShow
|
||||||
@@ -471,7 +524,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newColor
|
newColor
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.customColor = Color.parseColor(oldColor)
|
database.customColor = oldColor.toColorInt()
|
||||||
oldColor
|
oldColor
|
||||||
}
|
}
|
||||||
dbCustomColorPref?.summary = defaultColorToShow
|
dbCustomColorPref?.summary = defaultColorToShow
|
||||||
@@ -483,7 +536,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newCompression
|
newCompression
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.compressionAlgorithm = oldCompression
|
database.compressionAlgorithm = oldCompression
|
||||||
oldCompression
|
oldCompression
|
||||||
}
|
}
|
||||||
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
|
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
|
||||||
@@ -497,7 +550,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
} else {
|
} else {
|
||||||
oldRecycleBin
|
oldRecycleBin
|
||||||
}
|
}
|
||||||
mDatabase?.setRecycleBin(recycleBinToShow)
|
database.setRecycleBin(recycleBinToShow)
|
||||||
refreshRecycleBinGroup(database)
|
refreshRecycleBinGroup(database)
|
||||||
}
|
}
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
|
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
|
||||||
@@ -509,7 +562,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
} else {
|
} else {
|
||||||
oldTemplatesGroup
|
oldTemplatesGroup
|
||||||
}
|
}
|
||||||
mDatabase?.setTemplatesGroup(templatesGroupToShow)
|
database.setTemplatesGroup(templatesGroupToShow)
|
||||||
refreshTemplatesGroup(database)
|
refreshTemplatesGroup(database)
|
||||||
}
|
}
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
|
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
|
||||||
@@ -519,7 +572,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newMaxHistoryItems
|
newMaxHistoryItems
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.historyMaxItems = oldMaxHistoryItems
|
database.historyMaxItems = oldMaxHistoryItems
|
||||||
oldMaxHistoryItems
|
oldMaxHistoryItems
|
||||||
}
|
}
|
||||||
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
|
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
|
||||||
@@ -531,7 +584,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newMaxHistorySize
|
newMaxHistorySize
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.historyMaxSize = oldMaxHistorySize
|
database.historyMaxSize = oldMaxHistorySize
|
||||||
oldMaxHistorySize
|
oldMaxHistorySize
|
||||||
}
|
}
|
||||||
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
|
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
|
||||||
@@ -549,7 +602,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newEncryption
|
newEncryption
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.encryptionAlgorithm = oldEncryption
|
database.encryptionAlgorithm = oldEncryption
|
||||||
oldEncryption
|
oldEncryption
|
||||||
}
|
}
|
||||||
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
|
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
|
||||||
@@ -561,7 +614,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newKeyDerivationEngine
|
newKeyDerivationEngine
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.kdfEngine = oldKeyDerivationEngine
|
database.kdfEngine = oldKeyDerivationEngine
|
||||||
oldKeyDerivationEngine
|
oldKeyDerivationEngine
|
||||||
}
|
}
|
||||||
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
|
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
|
||||||
@@ -578,7 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newIterations
|
newIterations
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.numberKeyEncryptionRounds = oldIterations
|
database.numberKeyEncryptionRounds = oldIterations
|
||||||
oldIterations
|
oldIterations
|
||||||
}
|
}
|
||||||
mRoundPref?.summary = roundsToShow.toString()
|
mRoundPref?.summary = roundsToShow.toString()
|
||||||
@@ -590,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newMemoryUsage
|
newMemoryUsage
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.memoryUsage = oldMemoryUsage
|
database.memoryUsage = oldMemoryUsage
|
||||||
oldMemoryUsage
|
oldMemoryUsage
|
||||||
}
|
}
|
||||||
mMemoryPref?.summary = memoryToShow.toString()
|
mMemoryPref?.summary = memoryToShow.toString()
|
||||||
@@ -602,7 +655,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newParallelism
|
newParallelism
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.parallelism = oldParallelism
|
database.parallelism = oldParallelism
|
||||||
oldParallelism
|
oldParallelism
|
||||||
}
|
}
|
||||||
mParallelismPref?.summary = parallelismToShow.toString()
|
mParallelismPref?.summary = parallelismToShow.toString()
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.auto_focus_search_default))
|
context.resources.getBoolean(R.bool.auto_focus_search_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchSubdomains(context: Context): Boolean {
|
fun searchSubDomains(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
|
return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
|
||||||
context.resources.getBoolean(R.bool.subdomain_search_default))
|
context.resources.getBoolean(R.bool.subdomain_search_default))
|
||||||
@@ -352,6 +352,8 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.search_option_username_default))
|
context.resources.getBoolean(R.bool.search_option_username_default))
|
||||||
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
|
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
|
||||||
context.resources.getBoolean(R.bool.search_option_password_default))
|
context.resources.getBoolean(R.bool.search_option_password_default))
|
||||||
|
searchInAppIds = prefs.getBoolean(context.getString(R.string.search_option_application_id_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_application_id_default))
|
||||||
searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
|
searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
|
||||||
context.resources.getBoolean(R.bool.search_option_url_default))
|
context.resources.getBoolean(R.bool.search_option_url_default))
|
||||||
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
|
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
|
||||||
@@ -389,6 +391,8 @@ object PreferencesUtil {
|
|||||||
searchParameters.searchInUsernames)
|
searchParameters.searchInUsernames)
|
||||||
putBoolean(context.getString(R.string.search_option_password_key),
|
putBoolean(context.getString(R.string.search_option_password_key),
|
||||||
searchParameters.searchInPasswords)
|
searchParameters.searchInPasswords)
|
||||||
|
putBoolean(context.getString(R.string.search_option_application_id_key),
|
||||||
|
searchParameters.searchInAppIds)
|
||||||
putBoolean(context.getString(R.string.search_option_url_key),
|
putBoolean(context.getString(R.string.search_option_url_key),
|
||||||
searchParameters.searchInUrls)
|
searchParameters.searchInUrls)
|
||||||
putBoolean(context.getString(R.string.search_option_expired_key),
|
putBoolean(context.getString(R.string.search_option_expired_key),
|
||||||
@@ -686,6 +690,12 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
|
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isPasskeyCloseDatabaseEnable(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.passkeys_close_database_key),
|
||||||
|
context.resources.getBoolean(R.bool.passkeys_close_database_default))
|
||||||
|
}
|
||||||
|
|
||||||
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
|
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
|
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
|
||||||
@@ -854,6 +864,10 @@ object PreferencesUtil {
|
|||||||
context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.passkeys_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.passkeys_auto_select_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.passkeys_backup_eligibility_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.passkeys_backup_state_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
|||||||
@@ -70,8 +70,12 @@ open class SettingsActivity
|
|||||||
// To apply navigation bar with background color
|
// To apply navigation bar with background color
|
||||||
/* TODO Settings nav bar
|
/* TODO Settings nav bar
|
||||||
setTransparentNavigationBar {
|
setTransparentNavigationBar {
|
||||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
coordinatorLayout?.applyWindowInsets(EnumSet.of(
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
WindowInsetPosition.TOP_MARGINS,
|
||||||
|
WindowInsetPosition.BOTTOM_MARGINS,
|
||||||
|
WindowInsetPosition.START_MARGINS,
|
||||||
|
WindowInsetPosition.END_MARGINS,
|
||||||
|
))
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
@@ -155,10 +159,6 @@ open class SettingsActivity
|
|||||||
return coordinatorLayout
|
return coordinatorLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
@@ -188,7 +188,7 @@ open class SettingsActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||||
assignPassword(mainCredential)
|
assignMainCredential(mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||||
|
|||||||
@@ -95,20 +95,16 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
|
|||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
var initColor = database.customColor
|
||||||
|
if (initColor != null) {
|
||||||
database?.let {
|
enableSwitchView.isChecked = true
|
||||||
var initColor = it.customColor
|
} else {
|
||||||
if (initColor != null) {
|
enableSwitchView.isChecked = false
|
||||||
enableSwitchView.isChecked = true
|
initColor = DEFAULT_COLOR
|
||||||
} else {
|
|
||||||
enableSwitchView.isChecked = false
|
|
||||||
initColor = DEFAULT_COLOR
|
|
||||||
}
|
|
||||||
chromaColorView.currentColor = initColor
|
|
||||||
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
|
|
||||||
}
|
}
|
||||||
|
chromaColorView.currentColor = initColor
|
||||||
|
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -50,16 +50,14 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
setExplanationText(R.string.database_data_compression_summary)
|
setExplanationText(R.string.database_data_compression_summary)
|
||||||
|
|
||||||
mRecyclerView?.adapter = mCompressionAdapter
|
mRecyclerView?.adapter = mCompressionAdapter
|
||||||
|
compressionSelected = database.compressionAlgorithm
|
||||||
database?.let {
|
mCompressionAdapter?.setItems(
|
||||||
compressionSelected = it.compressionAlgorithm
|
items = database.availableCompressionAlgorithms,
|
||||||
mCompressionAdapter?.setItems(it.availableCompressionAlgorithms, compressionSelected)
|
itemUsed = compressionSelected
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
|
|
||||||
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
inputText = database.defaultUsername
|
||||||
inputText = database?.defaultUsername?: ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
|
|
||||||
class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
inputText = database.description
|
||||||
inputText = database?.description ?: ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -51,12 +51,9 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
algorithmSelected = database.encryptionAlgorithm
|
||||||
database?.let {
|
mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
|
||||||
algorithmSelected = database.encryptionAlgorithm
|
|
||||||
mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
kdfEngineSelected = database.kdfEngine
|
||||||
database?.let {
|
mKdfAdapter?.setItems(
|
||||||
kdfEngineSelected = database.kdfEngine
|
items = database.availableKdfEngines,
|
||||||
mKdfAdapter?.setItems(database.availableKdfEngines, kdfEngineSelected)
|
itemUsed = kdfEngineSelected
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -31,19 +31,17 @@ class DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePrefer
|
|||||||
setExplanationText(R.string.max_history_items_summary)
|
setExplanationText(R.string.max_history_items_summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
val maxItemsDatabase = database.historyMaxItems
|
||||||
database?.historyMaxItems?.let { maxItemsDatabase ->
|
inputText = maxItemsDatabase.toString()
|
||||||
inputText = maxItemsDatabase.toString()
|
setSwitchAction({ isChecked ->
|
||||||
setSwitchAction({ isChecked ->
|
inputText = if (!isChecked) {
|
||||||
inputText = if (!isChecked) {
|
NONE_MAX_HISTORY_ITEMS.toString()
|
||||||
NONE_MAX_HISTORY_ITEMS.toString()
|
} else {
|
||||||
} else {
|
DEFAULT_MAX_HISTORY_ITEMS.toString()
|
||||||
DEFAULT_MAX_HISTORY_ITEMS.toString()
|
}
|
||||||
}
|
showInputText(isChecked)
|
||||||
showInputText(isChecked)
|
}, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
|
||||||
}, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -34,31 +34,29 @@ class DatabaseMaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePrefere
|
|||||||
setExplanationText(R.string.max_history_size_summary)
|
setExplanationText(R.string.max_history_size_summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
val maxItemsDatabase = database.historyMaxSize
|
||||||
database?.historyMaxSize?.let { maxItemsDatabase ->
|
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
|
||||||
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
|
.toBetterByteFormat()
|
||||||
.toBetterByteFormat()
|
inputText = dataByte.number.toString()
|
||||||
inputText = dataByte.number.toString()
|
if (dataByte.number >= 0) {
|
||||||
if (dataByte.number >= 0) {
|
setUnitText(dataByte.format.stringId)
|
||||||
setUnitText(dataByte.format.stringId)
|
} else {
|
||||||
} else {
|
unitText = null
|
||||||
unitText = null
|
|
||||||
}
|
|
||||||
|
|
||||||
setSwitchAction({ isChecked ->
|
|
||||||
if (!isChecked) {
|
|
||||||
dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
|
|
||||||
inputText = INFINITE_MAX_HISTORY_SIZE.toString()
|
|
||||||
unitText = null
|
|
||||||
} else {
|
|
||||||
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
|
|
||||||
inputText = dataByte.number.toString()
|
|
||||||
setUnitText(dataByte.format.stringId)
|
|
||||||
}
|
|
||||||
showInputText(isChecked)
|
|
||||||
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSwitchAction({ isChecked ->
|
||||||
|
if (!isChecked) {
|
||||||
|
dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
|
||||||
|
inputText = INFINITE_MAX_HISTORY_SIZE.toString()
|
||||||
|
unitText = null
|
||||||
|
} else {
|
||||||
|
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
|
||||||
|
inputText = dataByte.number.toString()
|
||||||
|
setUnitText(dataByte.format.stringId)
|
||||||
|
}
|
||||||
|
showInputText(isChecked)
|
||||||
|
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -34,15 +34,12 @@ class DatabaseMemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreference
|
|||||||
setExplanationText(R.string.memory_usage_explanation)
|
setExplanationText(R.string.memory_usage_explanation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
val memoryBytes = database.memoryUsage
|
||||||
database?.let {
|
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
|
||||||
val memoryBytes = database.memoryUsage
|
.toBetterByteFormat()
|
||||||
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
|
inputText = dataByte.number.toString()
|
||||||
.toBetterByteFormat()
|
setUnitText(dataByte.format.stringId)
|
||||||
inputText = dataByte.number.toString()
|
|
||||||
setUnitText(dataByte.format.stringId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
|
|
||||||
class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
inputText = database.name
|
||||||
inputText = database?.name ?: ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -31,9 +31,8 @@ class DatabaseParallelismPreferenceDialogFragmentCompat : DatabaseSavePreference
|
|||||||
setExplanationText(R.string.parallelism_explanation)
|
setExplanationText(R.string.parallelism_explanation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
inputText = database.parallelism.toString()
|
||||||
inputText = database?.parallelism?.toString() ?: MIN_PARALLELISM.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -48,12 +48,9 @@ class DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
mGroupRecycleBin = database.recycleBin
|
||||||
database?.let {
|
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
|
||||||
mGroupRecycleBin = database.recycleBin
|
|
||||||
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemSelected(item: Group) {
|
override fun onItemSelected(item: Group) {
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat : DatabaseSavePre
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {
|
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ class DatabaseRoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
|
|||||||
explanationText = getString(R.string.rounds_explanation)
|
explanationText = getString(R.string.rounds_explanation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
inputText = database.numberKeyEncryptionRounds.toString()
|
||||||
inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -32,13 +35,15 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseSavePreferenceDialogFragmentCompat
|
abstract class DatabaseSavePreferenceDialogFragmentCompat
|
||||||
: InputPreferenceDialogFragmentCompat(), DatabaseRetrieval {
|
: InputPreferenceDialogFragmentCompat(), DatabaseRetrieval {
|
||||||
|
|
||||||
private var mDatabaseAutoSaveEnable = true
|
private var mDatabaseAutoSaveEnable = true
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
private var mDatabase: ContextualDatabase? = null
|
protected val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
@@ -47,18 +52,32 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
mDatabaseViewModel.database.observe(this) { database ->
|
lifecycleScope.launch {
|
||||||
onDatabaseRetrieved(database)
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
database?.let {
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
onDatabaseRetrieved(mDatabase)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
this.mDatabase = database
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
@@ -77,8 +96,10 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
|
|||||||
// To inherit to save element in database
|
// To inherit to save element in database
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveColor(oldColor: Int?,
|
protected fun saveColor(
|
||||||
newColor: Int?) {
|
oldColor: Int?,
|
||||||
|
newColor: Int?
|
||||||
|
) {
|
||||||
val oldColorString = if (oldColor != null)
|
val oldColorString = if (oldColor != null)
|
||||||
ChromaUtil.getFormattedColorString(oldColor, false)
|
ChromaUtil.getFormattedColorString(oldColor, false)
|
||||||
else
|
else
|
||||||
@@ -87,77 +108,158 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
|
|||||||
ChromaUtil.getFormattedColorString(newColor, false)
|
ChromaUtil.getFormattedColorString(newColor, false)
|
||||||
else
|
else
|
||||||
""
|
""
|
||||||
mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable)
|
mDatabaseViewModel.saveColor(
|
||||||
|
oldColorString,
|
||||||
|
newColorString,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveCompression(oldCompression: CompressionAlgorithm,
|
protected fun saveCompression(
|
||||||
newCompression: CompressionAlgorithm
|
oldCompression: CompressionAlgorithm,
|
||||||
|
newCompression: CompressionAlgorithm
|
||||||
) {
|
) {
|
||||||
mDatabaseViewModel.saveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
|
mDatabaseViewModel.saveCompression(
|
||||||
|
oldCompression,
|
||||||
|
newCompression,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveDefaultUsername(oldUsername: String,
|
protected fun saveDefaultUsername(
|
||||||
newUsername: String) {
|
oldUsername: String,
|
||||||
mDatabaseViewModel.saveDefaultUsername(oldUsername, newUsername, mDatabaseAutoSaveEnable)
|
newUsername: String
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveDefaultUsername(
|
||||||
|
oldUsername,
|
||||||
|
newUsername,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveDescription(oldDescription: String,
|
protected fun saveDescription(
|
||||||
newDescription: String) {
|
oldDescription: String,
|
||||||
mDatabaseViewModel.saveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
|
newDescription: String
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveDescription(
|
||||||
|
oldDescription,
|
||||||
|
newDescription,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveEncryption(oldEncryption: EncryptionAlgorithm,
|
protected fun saveEncryption(
|
||||||
newEncryptionAlgorithm: EncryptionAlgorithm) {
|
oldEncryption: EncryptionAlgorithm,
|
||||||
mDatabaseViewModel.saveEncryption(oldEncryption, newEncryptionAlgorithm, mDatabaseAutoSaveEnable)
|
newEncryptionAlgorithm: EncryptionAlgorithm
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveEncryption(
|
||||||
|
oldEncryption,
|
||||||
|
newEncryptionAlgorithm,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveKeyDerivation(oldKeyDerivation: KdfEngine,
|
protected fun saveKeyDerivation(
|
||||||
newKeyDerivation: KdfEngine) {
|
oldKeyDerivation: KdfEngine,
|
||||||
mDatabaseViewModel.saveKeyDerivation(oldKeyDerivation, newKeyDerivation, mDatabaseAutoSaveEnable)
|
newKeyDerivation: KdfEngine
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveKeyDerivation(
|
||||||
|
oldKeyDerivation,
|
||||||
|
newKeyDerivation,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveName(oldName: String,
|
protected fun saveName(
|
||||||
newName: String) {
|
oldName: String,
|
||||||
mDatabaseViewModel.saveName(oldName, newName, mDatabaseAutoSaveEnable)
|
newName: String
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveName(
|
||||||
|
oldName,
|
||||||
|
newName,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveRecycleBin(oldGroup: Group?,
|
protected fun saveRecycleBin(
|
||||||
newGroup: Group?) {
|
oldGroup: Group?,
|
||||||
mDatabaseViewModel.saveRecycleBin(oldGroup, newGroup, mDatabaseAutoSaveEnable)
|
newGroup: Group?
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveRecycleBin(
|
||||||
|
oldGroup,
|
||||||
|
newGroup,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun removeUnlinkedData() {
|
protected fun removeUnlinkedData() {
|
||||||
mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable)
|
mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveTemplatesGroup(oldGroup: Group?,
|
protected fun saveTemplatesGroup(
|
||||||
newGroup: Group?) {
|
oldGroup: Group?,
|
||||||
mDatabaseViewModel.saveTemplatesGroup(oldGroup, newGroup, mDatabaseAutoSaveEnable)
|
newGroup: Group?
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveTemplatesGroup(
|
||||||
|
oldGroup,
|
||||||
|
newGroup,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveMaxHistoryItems(oldNumber: Int,
|
protected fun saveMaxHistoryItems(
|
||||||
newNumber: Int) {
|
oldNumber: Int,
|
||||||
mDatabaseViewModel.saveMaxHistoryItems(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
newNumber: Int
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveMaxHistoryItems(
|
||||||
|
oldNumber,
|
||||||
|
newNumber,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveMaxHistorySize(oldNumber: Long,
|
protected fun saveMaxHistorySize(
|
||||||
newNumber: Long) {
|
oldNumber: Long,
|
||||||
mDatabaseViewModel.saveMaxHistorySize(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
newNumber: Long
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveMaxHistorySize(
|
||||||
|
oldNumber,
|
||||||
|
newNumber,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveMemoryUsage(oldNumber: Long,
|
protected fun saveMemoryUsage(
|
||||||
newNumber: Long) {
|
oldNumber: Long,
|
||||||
mDatabaseViewModel.saveMemoryUsage(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
newNumber: Long
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveMemoryUsage(
|
||||||
|
oldNumber,
|
||||||
|
newNumber,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveParallelism(oldNumber: Long,
|
protected fun saveParallelism(
|
||||||
newNumber: Long) {
|
oldNumber: Long,
|
||||||
mDatabaseViewModel.saveParallelism(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
newNumber: Long
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveParallelism(
|
||||||
|
oldNumber,
|
||||||
|
newNumber,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveIterations(oldNumber: Long,
|
protected fun saveIterations(
|
||||||
newNumber: Long) {
|
oldNumber: Long,
|
||||||
mDatabaseViewModel.saveIterations(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
newNumber: Long
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveIterations(
|
||||||
|
oldNumber,
|
||||||
|
newNumber,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -48,12 +48,9 @@ class DatabaseTemplatesGroupPreferenceDialogFragmentCompat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
mGroupTemplates = database.templatesGroup
|
||||||
database?.let {
|
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
|
||||||
mGroupTemplates = database.templatesGroup
|
|
||||||
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemSelected(item: Group) {
|
override fun onItemSelected(item: Group) {
|
||||||
|
|||||||
@@ -27,32 +27,27 @@ import android.view.View
|
|||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
open class ProgressTaskDialogFragment : DialogFragment() {
|
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 titleView: TextView? = null
|
||||||
private var messageView: TextView? = null
|
private var messageView: TextView? = null
|
||||||
private var warningView: TextView? = null
|
private var warningView: TextView? = null
|
||||||
private var cancelButton: Button? = null
|
private var cancelButton: Button? = null
|
||||||
private var progressView: ProgressBar? = 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 {
|
try {
|
||||||
activity?.let {
|
activity?.let {
|
||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
@@ -71,68 +66,44 @@ open class ProgressTaskDialogFragment : DialogFragment() {
|
|||||||
cancelButton = root.findViewById(R.id.progress_dialog_cancel)
|
cancelButton = root.findViewById(R.id.progress_dialog_cancel)
|
||||||
progressView = root.findViewById(R.id.progress_dialog_bar)
|
progressView = root.findViewById(R.id.progress_dialog_bar)
|
||||||
|
|
||||||
updateTitle(title)
|
|
||||||
updateMessage(message)
|
|
||||||
updateWarning(warning)
|
|
||||||
setCancellable(cancellable)
|
|
||||||
|
|
||||||
isCancelable = false
|
isCancelable = false
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
progressTaskViewModel.progressMessageState.collect { state ->
|
||||||
|
updateView(titleView,
|
||||||
|
state.titleId?.let { title -> getString(title) })
|
||||||
|
updateView(messageView,
|
||||||
|
state.messageId?.let { message -> getString(message) })
|
||||||
|
updateView(warningView,
|
||||||
|
state.warningId?.let { warning -> getString(warning) })
|
||||||
|
cancelButton?.isVisible = state.cancelable != null
|
||||||
|
cancelButton?.setOnClickListener {
|
||||||
|
state.cancelable?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to create progress dialog")
|
Log.e(TAG, "Unable to create progress dialog", e)
|
||||||
}
|
}
|
||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTitle(@StringRes titleId: Int) {
|
private fun updateView(textView: TextView?, value: String?) {
|
||||||
this.title = titleId
|
if (value == null) {
|
||||||
}
|
textView?.visibility = View.GONE
|
||||||
|
} else {
|
||||||
private fun updateView(textView: TextView?, @StringRes resId: Int) {
|
textView?.text = value
|
||||||
activity?.lifecycleScope?.launch {
|
textView?.visibility = View.VISIBLE
|
||||||
if (resId == UNDEFINED) {
|
|
||||||
textView?.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
textView?.setText(resId)
|
|
||||||
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 {
|
companion object {
|
||||||
private val TAG = ProgressTaskDialogFragment::class.java.simpleName
|
private val TAG = ProgressTaskDialogFragment::class.java.simpleName
|
||||||
const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment"
|
const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment"
|
||||||
const val UNDEFINED = -1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
class ProgressTaskViewModel: ViewModel() {
|
||||||
|
|
||||||
|
private val mProgressMessageState = MutableStateFlow(ProgressMessage())
|
||||||
|
val progressMessageState: StateFlow<ProgressMessage> = mProgressMessageState
|
||||||
|
|
||||||
|
private val mProgressTaskState = MutableStateFlow<ProgressTaskState>(ProgressTaskState.Stop)
|
||||||
|
val progressTaskState: StateFlow<ProgressTaskState> = mProgressTaskState
|
||||||
|
|
||||||
|
fun update(value: ProgressMessage) {
|
||||||
|
mProgressMessageState.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start(value: ProgressMessage) {
|
||||||
|
mProgressTaskState.value = ProgressTaskState.Start
|
||||||
|
update(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
mProgressTaskState.value = ProgressTaskState.Stop
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ProgressTaskState {
|
||||||
|
object Start: ProgressTaskState()
|
||||||
|
object Stop: ProgressTaskState()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,15 +13,13 @@ import com.kunzisoft.keepass.BuildConfig
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
import com.kunzisoft.keepass.education.Education
|
import com.kunzisoft.keepass.education.Education
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
|
||||||
|
|
||||||
object AppUtil {
|
object AppUtil {
|
||||||
|
|
||||||
|
fun randomRequestCode(): Int {
|
||||||
|
return (Math.random() * Integer.MAX_VALUE).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
|
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
|
||||||
try {
|
try {
|
||||||
this.applicationContext.packageManager.getPackageInfoCompat(
|
this.applicationContext.packageManager.getPackageInfoCompat(
|
||||||
@@ -79,29 +77,6 @@ object AppUtil {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the concrete web domain AKA without sub domain if needed
|
|
||||||
*/
|
|
||||||
fun getConcreteWebDomain(context: Context,
|
|
||||||
webDomain: String?,
|
|
||||||
concreteWebDomain: (String?) -> Unit) {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
if (webDomain != null) {
|
|
||||||
// Warning, web domain can contains IP, don't crop in this case
|
|
||||||
if (PreferencesUtil.searchSubdomains(context)
|
|
||||||
|| Regex(SearchInfo.WEB_IP_REGEX).matches(webDomain)) {
|
|
||||||
concreteWebDomain.invoke(webDomain)
|
|
||||||
} else {
|
|
||||||
val publicSuffixList = PublicSuffixList(context)
|
|
||||||
concreteWebDomain.invoke(publicSuffixList
|
|
||||||
.getPublicSuffixPlusOne(webDomain).await())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
concreteWebDomain.invoke(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.P)
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
|
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
@@ -123,25 +98,43 @@ object AppUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val processedPackageNames = mutableSetOf<String>()
|
val processedPackageNames = mutableSetOf<String>()
|
||||||
|
|
||||||
for (resolveInfo in resolveInfoList) {
|
for (resolveInfo in resolveInfoList) {
|
||||||
val packageName = resolveInfo.activityInfo.packageName
|
val packageName = resolveInfo.activityInfo.packageName
|
||||||
if (packageName != null && !processedPackageNames.contains(packageName)) {
|
if (packageName != null && !processedPackageNames.contains(packageName)) {
|
||||||
try {
|
buildAndroidPrivilegedApp(packageManager, packageName)?.let { privilegedApp ->
|
||||||
val packageInfo = packageManager.getPackageInfo(
|
browserList.add(privilegedApp)
|
||||||
packageName,
|
processedPackageNames.add(packageName)
|
||||||
PackageManager.GET_SIGNING_CERTIFICATES
|
|
||||||
)
|
|
||||||
val signatureFingerprints = packageInfo.signingInfo?.getAllFingerprints()
|
|
||||||
signatureFingerprints?.let {
|
|
||||||
browserList.add(AndroidPrivilegedApp(packageName, signatureFingerprints))
|
|
||||||
processedPackageNames.add(packageName)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppUtil::class.simpleName, "Error processing package: $packageName", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
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 {
|
||||||
|
AndroidPrivilegedApp(packageName, signatureFingerprints)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppUtil::class.simpleName, "Error processing package: $packageName", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,30 +19,39 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.utils
|
package com.kunzisoft.keepass.utils
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to invoke action in a separate IO thread
|
* Class to invoke action in a separate IO thread
|
||||||
*/
|
*/
|
||||||
class IOActionTask<T>(
|
class IOActionTask<T>(
|
||||||
private val action: () -> T ,
|
private val action: () -> T,
|
||||||
private val afterActionListener: ((T?) -> Unit)? = null) {
|
private val onActionComplete: ((T?) -> Unit)? = null,
|
||||||
|
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
|
||||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
private val exceptionHandler: CoroutineExceptionHandler? = null
|
||||||
|
) {
|
||||||
fun execute() {
|
fun execute() {
|
||||||
mainScope.launch {
|
scope.launch(exceptionHandler ?: EmptyCoroutineContext) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val asyncResult: Deferred<T?> = async {
|
val asyncResult: Deferred<T?> = async {
|
||||||
try {
|
exceptionHandler?.let {
|
||||||
action.invoke()
|
action.invoke()
|
||||||
} catch (e: Exception) {
|
} ?: try {
|
||||||
e.printStackTrace()
|
action.invoke()
|
||||||
null
|
} catch (e: Exception) {
|
||||||
}
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
afterActionListener?.invoke(asyncResult.await())
|
onActionComplete?.invoke(asyncResult.await())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.view
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@@ -30,8 +29,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
private var searchTitle: CompoundButton
|
private var searchTitle: CompoundButton
|
||||||
private var searchUsername: CompoundButton
|
private var searchUsername: CompoundButton
|
||||||
private var searchPassword: CompoundButton
|
private var searchPassword: CompoundButton
|
||||||
|
private var searchApplicationId: CompoundButton
|
||||||
private var searchURL: CompoundButton
|
private var searchURL: CompoundButton
|
||||||
private var searchByURLDomain: Boolean = false
|
private var searchByURLDomain: Boolean = false
|
||||||
|
private var searchByURLSubDomain: Boolean = false
|
||||||
private var searchExpired: CompoundButton
|
private var searchExpired: CompoundButton
|
||||||
private var searchNotes: CompoundButton
|
private var searchNotes: CompoundButton
|
||||||
private var searchOther: CompoundButton
|
private var searchOther: CompoundButton
|
||||||
@@ -50,8 +51,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
this.searchInTitles = searchTitle.isChecked
|
this.searchInTitles = searchTitle.isChecked
|
||||||
this.searchInUsernames = searchUsername.isChecked
|
this.searchInUsernames = searchUsername.isChecked
|
||||||
this.searchInPasswords = searchPassword.isChecked
|
this.searchInPasswords = searchPassword.isChecked
|
||||||
|
this.searchInAppIds = searchApplicationId.isChecked
|
||||||
this.searchInUrls = searchURL.isChecked
|
this.searchInUrls = searchURL.isChecked
|
||||||
this.searchByDomain = searchByURLDomain
|
this.searchByDomain = searchByURLDomain
|
||||||
|
this.searchBySubDomain = searchByURLSubDomain
|
||||||
this.searchInExpired = searchExpired.isChecked
|
this.searchInExpired = searchExpired.isChecked
|
||||||
this.searchInNotes = searchNotes.isChecked
|
this.searchInNotes = searchNotes.isChecked
|
||||||
this.searchInOther = searchOther.isChecked
|
this.searchInOther = searchOther.isChecked
|
||||||
@@ -71,8 +74,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchTitle.isChecked = value.searchInTitles
|
searchTitle.isChecked = value.searchInTitles
|
||||||
searchUsername.isChecked = value.searchInUsernames
|
searchUsername.isChecked = value.searchInUsernames
|
||||||
searchPassword.isChecked = value.searchInPasswords
|
searchPassword.isChecked = value.searchInPasswords
|
||||||
|
searchApplicationId.isChecked = value.searchInAppIds
|
||||||
searchURL.isChecked = value.searchInUrls
|
searchURL.isChecked = value.searchInUrls
|
||||||
searchByURLDomain = value.searchByDomain
|
searchByURLDomain = value.searchByDomain
|
||||||
|
searchByURLSubDomain = value.searchBySubDomain
|
||||||
searchExpired.isChecked = value.searchInExpired
|
searchExpired.isChecked = value.searchInExpired
|
||||||
searchNotes.isChecked = value.searchInNotes
|
searchNotes.isChecked = value.searchInNotes
|
||||||
searchOther.isChecked = value.searchInOther
|
searchOther.isChecked = value.searchInOther
|
||||||
@@ -87,7 +92,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
|
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
|
||||||
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
|
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
|
||||||
// To recalculate height
|
// To recalculate height
|
||||||
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
|
if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
|
||||||
searchAdvanceFiltersContainer?.expand(
|
searchAdvanceFiltersContainer?.expand(
|
||||||
false,
|
false,
|
||||||
searchAdvanceFiltersContainer?.getFullHeight()
|
searchAdvanceFiltersContainer?.getFullHeight()
|
||||||
@@ -110,6 +115,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchTitle = findViewById(R.id.search_chip_title)
|
searchTitle = findViewById(R.id.search_chip_title)
|
||||||
searchUsername = findViewById(R.id.search_chip_username)
|
searchUsername = findViewById(R.id.search_chip_username)
|
||||||
searchPassword = findViewById(R.id.search_chip_password)
|
searchPassword = findViewById(R.id.search_chip_password)
|
||||||
|
searchApplicationId = findViewById(R.id.search_chip_application_id)
|
||||||
searchURL = findViewById(R.id.search_chip_url)
|
searchURL = findViewById(R.id.search_chip_url)
|
||||||
searchExpired = findViewById(R.id.search_chip_expires)
|
searchExpired = findViewById(R.id.search_chip_expires)
|
||||||
searchNotes = findViewById(R.id.search_chip_note)
|
searchNotes = findViewById(R.id.search_chip_note)
|
||||||
@@ -125,7 +131,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
// Expand menu with button
|
// Expand menu with button
|
||||||
searchExpandButton.setOnClickListener {
|
searchExpandButton.setOnClickListener {
|
||||||
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE
|
val isVisible = searchAdvanceFiltersContainer?.visibility == VISIBLE
|
||||||
if (isVisible)
|
if (isVisible)
|
||||||
closeAdvancedFilters()
|
closeAdvancedFilters()
|
||||||
else
|
else
|
||||||
@@ -156,6 +162,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchParameters.searchInPasswords = isChecked
|
searchParameters.searchInPasswords = isChecked
|
||||||
mOnParametersChangeListener?.invoke(searchParameters)
|
mOnParametersChangeListener?.invoke(searchParameters)
|
||||||
}
|
}
|
||||||
|
searchApplicationId.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
searchParameters.searchInAppIds = isChecked
|
||||||
|
mOnParametersChangeListener?.invoke(searchParameters)
|
||||||
|
}
|
||||||
searchURL.setOnCheckedChangeListener { _, isChecked ->
|
searchURL.setOnCheckedChangeListener { _, isChecked ->
|
||||||
searchParameters.searchInUrls = isChecked
|
searchParameters.searchInUrls = isChecked
|
||||||
mOnParametersChangeListener?.invoke(searchParameters)
|
mOnParametersChangeListener?.invoke(searchParameters)
|
||||||
@@ -200,10 +210,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
|
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCurrentGroupText(text: String) {
|
fun setCurrentGroupText(text: String?) {
|
||||||
val maxChars = 12
|
val maxChars = 12
|
||||||
searchCurrentGroup.text = when {
|
searchCurrentGroup.text = when {
|
||||||
text.isEmpty() -> context.getString(R.string.current_group)
|
text.isNullOrEmpty() -> context.getString(R.string.current_group)
|
||||||
text.length > maxChars -> text.substring(0, maxChars) + "…"
|
text.length > maxChars -> text.substring(0, maxChars) + "…"
|
||||||
else -> text
|
else -> text
|
||||||
}
|
}
|
||||||
@@ -213,6 +223,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchOther.isVisible = available
|
searchOther.isVisible = available
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun availableApplicationIds(available: Boolean) {
|
||||||
|
searchApplicationId.isVisible = available
|
||||||
|
}
|
||||||
|
|
||||||
fun availableTags(available: Boolean) {
|
fun availableTags(available: Boolean) {
|
||||||
searchTag.isVisible = available
|
searchTag.isVisible = available
|
||||||
}
|
}
|
||||||
@@ -243,16 +257,20 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSearchExpandButton(show: Boolean) {
|
||||||
|
searchExpandButton.isVisible = show
|
||||||
|
}
|
||||||
|
|
||||||
override fun setVisibility(visibility: Int) {
|
override fun setVisibility(visibility: Int) {
|
||||||
when (visibility) {
|
when (visibility) {
|
||||||
View.VISIBLE -> {
|
VISIBLE -> {
|
||||||
searchAdvanceFiltersContainer?.visibility = View.GONE
|
searchAdvanceFiltersContainer?.visibility = GONE
|
||||||
searchContainer.showByFading()
|
searchContainer.showByFading()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
searchContainer.hideByFading()
|
searchContainer.hideByFading()
|
||||||
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
|
if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
|
||||||
searchAdvanceFiltersContainer?.visibility = View.INVISIBLE
|
searchAdvanceFiltersContainer?.visibility = INVISIBLE
|
||||||
searchAdvanceFiltersContainer?.collapse()
|
searchAdvanceFiltersContainer?.collapse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.animation.AnimatorSet
|
|||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
@@ -66,6 +65,7 @@ import com.kunzisoft.keepass.database.exception.LocalizedException
|
|||||||
import com.kunzisoft.keepass.database.helper.getLocalizedMessage
|
import com.kunzisoft.keepass.database.helper.getLocalizedMessage
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -317,9 +317,7 @@ fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
|
|||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
|
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
|
||||||
// Only in portrait
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
|
|
||||||
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
|
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
|
||||||
if (applyToStatusBar) {
|
if (applyToStatusBar) {
|
||||||
@@ -335,7 +333,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
|
|||||||
/**
|
/**
|
||||||
* Apply a margin to a view to fix the window inset
|
* Apply a margin to a view to fix the window inset
|
||||||
*/
|
*/
|
||||||
fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.BOTTOM) {
|
fun View.applyWindowInsets(positions: EnumSet<WindowInsetPosition>) {
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
|
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
|
||||||
var consumed = false
|
var consumed = false
|
||||||
|
|
||||||
@@ -351,52 +349,78 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()
|
||||||
when (position) {
|
or WindowInsetsCompat.Type.displayCutout()
|
||||||
WindowInsetPosition.TOP -> {
|
or WindowInsetsCompat.Type.ime())
|
||||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
|
||||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||||
topMargin = insets.top
|
|
||||||
|
val wantTopMargins = positions.contains(WindowInsetPosition.TOP_MARGINS)
|
||||||
|
val wantBottomMargins = positions.contains(WindowInsetPosition.BOTTOM_MARGINS)
|
||||||
|
val wantStartMargins = positions.contains(WindowInsetPosition.START_MARGINS)
|
||||||
|
val wantEndMargins = positions.contains(WindowInsetPosition.END_MARGINS)
|
||||||
|
|
||||||
|
if (view.layoutParams is ViewGroup.MarginLayoutParams
|
||||||
|
&& (wantTopMargins || wantBottomMargins || wantStartMargins || wantEndMargins)) {
|
||||||
|
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
if (wantTopMargins) {
|
||||||
|
topMargin = insets.top
|
||||||
|
}
|
||||||
|
if (wantBottomMargins) {
|
||||||
|
bottomMargin = insets.bottom
|
||||||
|
}
|
||||||
|
if (wantStartMargins) {
|
||||||
|
if (isRtl) {
|
||||||
|
rightMargin = insets.right
|
||||||
|
} else {
|
||||||
|
leftMargin = insets.left
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (wantEndMargins) {
|
||||||
WindowInsetPosition.LEGIT_TOP -> {
|
if (isRtl) {
|
||||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
leftMargin = insets.left
|
||||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
} else {
|
||||||
topMargin = 0
|
rightMargin = insets.right
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WindowInsetPosition.BOTTOM -> {
|
|
||||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
|
||||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = insets.bottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WindowInsetPosition.BOTTOM_IME -> {
|
|
||||||
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
||||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
|
||||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = if (imeHeight > 1) 0 else insets.bottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WindowInsetPosition.TOP_BOTTOM_IME -> {
|
|
||||||
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
||||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
|
||||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
topMargin = insets.top
|
|
||||||
bottomMargin = if (imeHeight > 1) imeHeight else 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val wantTopPadding = positions.contains(WindowInsetPosition.TOP_PADDING)
|
||||||
|
val wantBottomPadding = positions.contains(WindowInsetPosition.BOTTOM_PADDING)
|
||||||
|
val wantStartPadding = positions.contains(WindowInsetPosition.START_PADDING)
|
||||||
|
val wantEndPadding = positions.contains(WindowInsetPosition.END_PADDING)
|
||||||
|
|
||||||
|
if (wantTopPadding || wantBottomPadding || wantStartPadding || wantEndPadding) {
|
||||||
|
val topPadding = if (wantTopPadding) insets.top else 0
|
||||||
|
val bottomPadding = if (wantBottomPadding) insets.bottom else 0
|
||||||
|
var leftPadding = 0
|
||||||
|
var rightPadding = 0
|
||||||
|
|
||||||
|
if (wantStartPadding) {
|
||||||
|
if (isRtl) {
|
||||||
|
rightPadding = insets.right
|
||||||
|
} else {
|
||||||
|
leftPadding = insets.left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wantEndPadding) {
|
||||||
|
if (isRtl) {
|
||||||
|
leftPadding = insets.left
|
||||||
|
} else {
|
||||||
|
rightPadding = insets.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPadding(leftPadding, topPadding, rightPadding, bottomPadding)
|
||||||
|
}
|
||||||
|
|
||||||
// If any of the children consumed the insets, return an appropriate value
|
// If any of the children consumed the insets, return an appropriate value
|
||||||
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
|
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class WindowInsetPosition {
|
enum class WindowInsetPosition {
|
||||||
TOP, BOTTOM, LEGIT_TOP, BOTTOM_IME, TOP_BOTTOM_IME
|
TOP_MARGINS, BOTTOM_MARGINS, START_MARGINS, END_MARGINS,
|
||||||
|
TOP_PADDING, BOTTOM_PADDING, START_PADDING, END_PADDING,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,214 +1,500 @@
|
|||||||
package com.kunzisoft.keepass.viewmodels
|
package com.kunzisoft.keepass.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import android.app.Application
|
||||||
import androidx.lifecycle.MutableLiveData
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import android.os.Bundle
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
|
import com.kunzisoft.keepass.database.ProgressMessage
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class DatabaseViewModel: ViewModel() {
|
class DatabaseViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
val database : LiveData<ContextualDatabase?> get() = _database
|
private val mDatabaseState = MutableStateFlow<ContextualDatabase?>(null)
|
||||||
private val _database = MutableLiveData<ContextualDatabase?>()
|
val databaseState: StateFlow<ContextualDatabase?> = mDatabaseState
|
||||||
|
|
||||||
val actionFinished : LiveData<ActionResult> get() = _actionFinished
|
val database: ContextualDatabase?
|
||||||
private val _actionFinished = SingleLiveEvent<ActionResult>()
|
get() = databaseState.value
|
||||||
|
|
||||||
val saveDatabase : LiveData<Boolean> get() = _saveDatabase
|
private val mActionState = MutableStateFlow<ActionState>(ActionState.Loading)
|
||||||
private val _saveDatabase = SingleLiveEvent<Boolean>()
|
val actionState: StateFlow<ActionState> = mActionState
|
||||||
|
|
||||||
val mergeDatabase : LiveData<Boolean> get() = _mergeDatabase
|
private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider(
|
||||||
private val _mergeDatabase = SingleLiveEvent<Boolean>()
|
context = application
|
||||||
|
)
|
||||||
|
|
||||||
val reloadDatabase : LiveData<Boolean> get() = _reloadDatabase
|
init {
|
||||||
private val _reloadDatabase = SingleLiveEvent<Boolean>()
|
mDatabaseTaskProvider.onDatabaseRetrieved = { databaseRetrieved ->
|
||||||
|
val databaseWasReloaded = databaseRetrieved?.wasReloaded == true
|
||||||
|
if (databaseWasReloaded) {
|
||||||
|
mActionState.value = ActionState.OnDatabaseReloaded
|
||||||
|
}
|
||||||
|
if (database == null || database != databaseRetrieved || databaseWasReloaded) {
|
||||||
|
databaseRetrieved?.wasReloaded = false
|
||||||
|
mDatabaseState.value = databaseRetrieved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mDatabaseTaskProvider.onStartActionRequested = { bundle, actionTask ->
|
||||||
|
mActionState.value = ActionState.OnDatabaseActionRequested(bundle, actionTask)
|
||||||
|
}
|
||||||
|
mDatabaseTaskProvider.databaseInfoListener = object : DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||||
|
override fun onDatabaseInfoChanged(
|
||||||
|
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
readOnlyDatabase: Boolean
|
||||||
|
) {
|
||||||
|
mActionState.value = ActionState.OnDatabaseInfoChanged(
|
||||||
|
previousDatabaseInfo,
|
||||||
|
newDatabaseInfo,
|
||||||
|
readOnlyDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mDatabaseTaskProvider.actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
|
||||||
|
override fun onActionStarted(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
progressMessage: ProgressMessage
|
||||||
|
) {
|
||||||
|
mActionState.value = ActionState.OnDatabaseActionStarted(database, progressMessage)
|
||||||
|
}
|
||||||
|
|
||||||
val saveName : LiveData<SuperString> get() = _saveName
|
override fun onActionUpdated(
|
||||||
private val _saveName = SingleLiveEvent<SuperString>()
|
database: ContextualDatabase,
|
||||||
|
progressMessage: ProgressMessage
|
||||||
|
) {
|
||||||
|
mActionState.value = ActionState.OnDatabaseActionUpdated(database, progressMessage)
|
||||||
|
}
|
||||||
|
|
||||||
val saveDescription : LiveData<SuperString> get() = _saveDescription
|
override fun onActionStopped(database: ContextualDatabase?) {
|
||||||
private val _saveDescription = SingleLiveEvent<SuperString>()
|
mActionState.value = ActionState.OnDatabaseActionStopped(database)
|
||||||
|
}
|
||||||
|
|
||||||
val saveDefaultUsername : LiveData<SuperString> get() = _saveDefaultUsername
|
override fun onActionFinished(
|
||||||
private val _saveDefaultUsername = SingleLiveEvent<SuperString>()
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
mActionState.value = ActionState.OnDatabaseActionFinished(database, actionTask, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val saveColor : LiveData<SuperString> get() = _saveColor
|
mDatabaseTaskProvider.registerProgressTask()
|
||||||
private val _saveColor = SingleLiveEvent<SuperString>()
|
|
||||||
|
|
||||||
val saveCompression : LiveData<SuperCompression> get() = _saveCompression
|
|
||||||
private val _saveCompression = SingleLiveEvent<SuperCompression>()
|
|
||||||
|
|
||||||
val removeUnlinkData : LiveData<Boolean> get() = _removeUnlinkData
|
|
||||||
private val _removeUnlinkData = SingleLiveEvent<Boolean>()
|
|
||||||
|
|
||||||
val saveRecycleBin : LiveData<SuperGroup> get() = _saveRecycleBin
|
|
||||||
private val _saveRecycleBin = SingleLiveEvent<SuperGroup>()
|
|
||||||
|
|
||||||
val saveTemplatesGroup : LiveData<SuperGroup> get() = _saveTemplatesGroup
|
|
||||||
private val _saveTemplatesGroup = SingleLiveEvent<SuperGroup>()
|
|
||||||
|
|
||||||
val saveMaxHistoryItems : LiveData<SuperInt> get() = _saveMaxHistoryItems
|
|
||||||
private val _saveMaxHistoryItems = SingleLiveEvent<SuperInt>()
|
|
||||||
|
|
||||||
val saveMaxHistorySize : LiveData<SuperLong> get() = _saveMaxHistorySize
|
|
||||||
private val _saveMaxHistorySize = SingleLiveEvent<SuperLong>()
|
|
||||||
|
|
||||||
val saveEncryption : LiveData<SuperEncryption> get() = _saveEncryption
|
|
||||||
private val _saveEncryption = SingleLiveEvent<SuperEncryption>()
|
|
||||||
|
|
||||||
val saveKeyDerivation : LiveData<SuperKeyDerivation> get() = _saveKeyDerivation
|
|
||||||
private val _saveKeyDerivation = SingleLiveEvent<SuperKeyDerivation>()
|
|
||||||
|
|
||||||
val saveIterations : LiveData<SuperLong> get() = _saveIterations
|
|
||||||
private val _saveIterations = SingleLiveEvent<SuperLong>()
|
|
||||||
|
|
||||||
val saveMemoryUsage : LiveData<SuperLong> get() = _saveMemoryUsage
|
|
||||||
private val _saveMemoryUsage = SingleLiveEvent<SuperLong>()
|
|
||||||
|
|
||||||
val saveParallelism : LiveData<SuperLong> get() = _saveParallelism
|
|
||||||
private val _saveParallelism = SingleLiveEvent<SuperLong>()
|
|
||||||
|
|
||||||
|
|
||||||
fun defineDatabase(database: ContextualDatabase?) {
|
|
||||||
this._database.value = database
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onActionFinished(database: ContextualDatabase,
|
/*
|
||||||
actionTask: String,
|
* Main database actions
|
||||||
result: ActionRunnable.Result) {
|
*/
|
||||||
this._actionFinished.value = ActionResult(database, actionTask, result)
|
|
||||||
|
fun loadDatabase(
|
||||||
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential,
|
||||||
|
readOnly: Boolean,
|
||||||
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
|
fixDuplicateUuid: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseLoad(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
fixDuplicateUuid
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDatabase(save: Boolean) {
|
fun createDatabase(
|
||||||
_saveDatabase.value = save
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseCreate(databaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeDatabase(save: Boolean) {
|
fun assignMainCredential(
|
||||||
_mergeDatabase.value = save
|
databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
) {
|
||||||
|
if (databaseUri != null) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseAssignCredential(databaseUri, mainCredential)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDatabase(save: Boolean, saveToUri: Uri? = null) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSave(save, saveToUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mergeDatabase(
|
||||||
|
save: Boolean,
|
||||||
|
fromDatabaseUri: Uri? = null,
|
||||||
|
mainCredential: MainCredential? = null
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseMerge(save, fromDatabaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadDatabase(fixDuplicateUuid: Boolean) {
|
fun reloadDatabase(fixDuplicateUuid: Boolean) {
|
||||||
_reloadDatabase.value = fixDuplicateUuid
|
mDatabaseTaskProvider.askToStartDatabaseReload(
|
||||||
|
conditionToAsk = database?.dataModifiedSinceLastLoading != false
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseReload(fixDuplicateUuid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveName(oldValue: String,
|
fun onDatabaseChangeValidated() {
|
||||||
newValue: String,
|
mDatabaseTaskProvider.onDatabaseChangeValidated()
|
||||||
save: Boolean) {
|
|
||||||
_saveName.value = SuperString(oldValue, newValue, save)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDescription(oldValue: String,
|
/*
|
||||||
newValue: String,
|
* Nodes actions
|
||||||
save: Boolean) {
|
*/
|
||||||
_saveDescription.value = SuperString(oldValue, newValue, save)
|
|
||||||
|
fun createEntry(
|
||||||
|
newEntry: Entry,
|
||||||
|
parent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseCreateEntry(
|
||||||
|
newEntry,
|
||||||
|
parent,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDefaultUsername(oldValue: String,
|
fun updateEntry(
|
||||||
newValue: String,
|
oldEntry: Entry,
|
||||||
save: Boolean) {
|
entryToUpdate: Entry,
|
||||||
_saveDefaultUsername.value = SuperString(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseUpdateEntry(
|
||||||
|
oldEntry,
|
||||||
|
entryToUpdate,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveColor(oldValue: String,
|
fun restoreEntryHistory(
|
||||||
newValue: String,
|
mainEntryId: NodeId<UUID>,
|
||||||
save: Boolean) {
|
entryHistoryPosition: Int,
|
||||||
_saveColor.value = SuperString(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseRestoreEntryHistory(
|
||||||
|
mainEntryId,
|
||||||
|
entryHistoryPosition,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCompression(oldValue: CompressionAlgorithm,
|
fun deleteEntryHistory(
|
||||||
newValue: CompressionAlgorithm,
|
mainEntryId: NodeId<UUID>,
|
||||||
save: Boolean) {
|
entryHistoryPosition: Int,
|
||||||
_saveCompression.value = SuperCompression(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseDeleteEntryHistory(
|
||||||
|
mainEntryId,
|
||||||
|
entryHistoryPosition,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createGroup(
|
||||||
|
newGroup: Group,
|
||||||
|
parent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseCreateGroup(
|
||||||
|
newGroup,
|
||||||
|
parent,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateGroup(
|
||||||
|
oldGroup: Group,
|
||||||
|
groupToUpdate: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseUpdateGroup(
|
||||||
|
oldGroup,
|
||||||
|
groupToUpdate,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyNodes(
|
||||||
|
nodesToCopy: List<Node>,
|
||||||
|
newParent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseCopyNodes(
|
||||||
|
nodesToCopy,
|
||||||
|
newParent,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveNodes(
|
||||||
|
nodesToMove: List<Node>,
|
||||||
|
newParent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseMoveNodes(
|
||||||
|
nodesToMove,
|
||||||
|
newParent,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteNodes(
|
||||||
|
nodes: List<Node>,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseDeleteNodes(
|
||||||
|
nodes,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Attributes
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun buildNewAttachment(): BinaryData? {
|
||||||
|
return database?.buildNewBinaryAttachment()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Settings actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun saveName(
|
||||||
|
oldValue: String,
|
||||||
|
newValue: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveName(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDescription(
|
||||||
|
oldValue: String,
|
||||||
|
newValue: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveDescription(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDefaultUsername(
|
||||||
|
oldValue: String,
|
||||||
|
newValue: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveDefaultUsername(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveColor(
|
||||||
|
oldValue: String,
|
||||||
|
newValue: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveColor(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCompression(
|
||||||
|
oldValue: CompressionAlgorithm,
|
||||||
|
newValue: CompressionAlgorithm,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveCompression(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeUnlinkedData(save: Boolean) {
|
fun removeUnlinkedData(save: Boolean) {
|
||||||
_removeUnlinkData.value = save
|
mDatabaseTaskProvider.startDatabaseRemoveUnlinkedData(save)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveRecycleBin(oldValue: Group?,
|
fun saveRecycleBin(
|
||||||
newValue: Group?,
|
oldValue: Group?,
|
||||||
save: Boolean) {
|
newValue: Group?,
|
||||||
_saveRecycleBin.value = SuperGroup(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveRecycleBin(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveTemplatesGroup(oldValue: Group?,
|
fun saveTemplatesGroup(
|
||||||
newValue: Group?,
|
oldValue: Group?,
|
||||||
save: Boolean) {
|
newValue: Group?,
|
||||||
_saveTemplatesGroup.value = SuperGroup(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveTemplatesGroup(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMaxHistoryItems(oldValue: Int,
|
fun saveMaxHistoryItems(
|
||||||
newValue: Int,
|
oldValue: Int,
|
||||||
save: Boolean) {
|
newValue: Int,
|
||||||
_saveMaxHistoryItems.value = SuperInt(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveMaxHistoryItems(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMaxHistorySize(oldValue: Long,
|
fun saveMaxHistorySize(
|
||||||
newValue: Long,
|
oldValue: Long,
|
||||||
save: Boolean) {
|
newValue: Long,
|
||||||
_saveMaxHistorySize.value = SuperLong(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveMaxHistorySize(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun saveEncryption(oldValue: EncryptionAlgorithm,
|
fun saveEncryption(
|
||||||
newValue: EncryptionAlgorithm,
|
oldValue: EncryptionAlgorithm,
|
||||||
save: Boolean) {
|
newValue: EncryptionAlgorithm,
|
||||||
_saveEncryption.value = SuperEncryption(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveEncryption(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveKeyDerivation(oldValue: KdfEngine,
|
fun saveKeyDerivation(
|
||||||
newValue: KdfEngine,
|
oldValue: KdfEngine,
|
||||||
save: Boolean) {
|
newValue: KdfEngine,
|
||||||
_saveKeyDerivation.value = SuperKeyDerivation(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveKeyDerivation(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveIterations(oldValue: Long,
|
fun saveIterations(
|
||||||
newValue: Long,
|
oldValue: Long,
|
||||||
save: Boolean) {
|
newValue: Long,
|
||||||
_saveIterations.value = SuperLong(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveIterations(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMemoryUsage(oldValue: Long,
|
fun saveMemoryUsage(
|
||||||
newValue: Long,
|
oldValue: Long,
|
||||||
save: Boolean) {
|
newValue: Long,
|
||||||
_saveMemoryUsage.value = SuperLong(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveMemoryUsage(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveParallelism(oldValue: Long,
|
fun saveParallelism(
|
||||||
newValue: Long,
|
oldValue: Long,
|
||||||
save: Boolean) {
|
newValue: Long,
|
||||||
_saveParallelism.value = SuperLong(oldValue, newValue, save)
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveParallelism(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ActionResult(val database: ContextualDatabase,
|
/*
|
||||||
val actionTask: String,
|
* Hardware Key
|
||||||
val result: ActionRunnable.Result)
|
*/
|
||||||
data class SuperString(val oldValue: String,
|
|
||||||
val newValue: String,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperInt(val oldValue: Int,
|
|
||||||
val newValue: Int,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperLong(val oldValue: Long,
|
|
||||||
val newValue: Long,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperMerge(val fixDuplicateUuid: Boolean,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperCompression(val oldValue: CompressionAlgorithm,
|
|
||||||
val newValue: CompressionAlgorithm,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperEncryption(val oldValue: EncryptionAlgorithm,
|
|
||||||
val newValue: EncryptionAlgorithm,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperKeyDerivation(val oldValue: KdfEngine,
|
|
||||||
val newValue: KdfEngine,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperGroup(val oldValue: Group?,
|
|
||||||
val newValue: Group?,
|
|
||||||
val save: Boolean)
|
|
||||||
|
|
||||||
|
fun onChallengeResponded(challengeResponse: ByteArray?) {
|
||||||
|
mDatabaseTaskProvider.startChallengeResponded(
|
||||||
|
challengeResponse ?: ByteArray(0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
mDatabaseTaskProvider.unregisterProgressTask()
|
||||||
|
mDatabaseTaskProvider.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ActionState {
|
||||||
|
object Loading: ActionState()
|
||||||
|
object OnDatabaseReloaded: ActionState()
|
||||||
|
data class OnDatabaseActionRequested(
|
||||||
|
val bundle: Bundle? = null,
|
||||||
|
val actionTask: String
|
||||||
|
): ActionState()
|
||||||
|
data class OnDatabaseInfoChanged(
|
||||||
|
val previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
val newDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
val readOnlyDatabase: Boolean
|
||||||
|
): ActionState()
|
||||||
|
data class OnDatabaseActionStarted(
|
||||||
|
var database: ContextualDatabase,
|
||||||
|
val progressMessage: ProgressMessage
|
||||||
|
): ActionState()
|
||||||
|
data class OnDatabaseActionUpdated(
|
||||||
|
var database: ContextualDatabase,
|
||||||
|
val progressMessage: ProgressMessage
|
||||||
|
): ActionState()
|
||||||
|
data class OnDatabaseActionStopped(
|
||||||
|
var database: ContextualDatabase?
|
||||||
|
): ActionState()
|
||||||
|
data class OnDatabaseActionFinished(
|
||||||
|
var database: ContextualDatabase,
|
||||||
|
val actionTask: String,
|
||||||
|
val result: ActionRunnable.Result
|
||||||
|
): ActionState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.kunzisoft.keepass.viewmodels
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
@@ -16,10 +17,11 @@ import com.kunzisoft.keepass.model.AttachmentState
|
|||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.utils.IOActionTask
|
import com.kunzisoft.keepass.utils.IOActionTask
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
@@ -28,12 +30,18 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
private var mEntryId: NodeId<UUID>? = null
|
private var mEntryId: NodeId<UUID>? = null
|
||||||
private var mParentId: NodeId<*>? = null
|
private var mParentId: NodeId<*>? = null
|
||||||
private var mRegisterInfo: RegisterInfo? = null
|
private var mRegisterInfo: RegisterInfo? = null
|
||||||
private var mSearchInfo: SearchInfo? = null
|
|
||||||
private var mParent: Group? = null
|
private var mParent: Group? = null
|
||||||
private var mEntry: Entry? = null
|
private var mEntry: Entry? = null
|
||||||
private var mIsTemplate: Boolean = false
|
private var mIsTemplate: Boolean = false
|
||||||
private val mTempAttachments = mutableListOf<EntryAttachmentState>()
|
private val mTempAttachments = mutableListOf<EntryAttachmentState>()
|
||||||
|
|
||||||
|
// To show dialog only one time
|
||||||
|
var backPressedAlreadyApproved = false
|
||||||
|
var warningOverwriteDataAlreadyApproved = false
|
||||||
|
|
||||||
|
// Useful to not relaunch a current action
|
||||||
|
private var actionLocked: Boolean = false
|
||||||
|
|
||||||
val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
|
val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
|
||||||
private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
|
private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
|
||||||
|
|
||||||
@@ -73,24 +81,28 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
|
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
|
||||||
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
|
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
|
||||||
|
|
||||||
fun loadDatabase(database: ContextualDatabase?) {
|
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||||
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo)
|
val uiState: StateFlow<UIState> = mUiState
|
||||||
|
|
||||||
|
fun loadTemplateEntry(database: ContextualDatabase?) {
|
||||||
|
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadTemplateEntry(database: ContextualDatabase?,
|
fun loadTemplateEntry(
|
||||||
entryId: NodeId<UUID>?,
|
database: ContextualDatabase?,
|
||||||
parentId: NodeId<*>?,
|
entryId: NodeId<UUID>?,
|
||||||
registerInfo: RegisterInfo?,
|
parentId: NodeId<*>?,
|
||||||
searchInfo: SearchInfo?) {
|
registerInfo: RegisterInfo?
|
||||||
|
) {
|
||||||
this.mEntryId = entryId
|
this.mEntryId = entryId
|
||||||
this.mParentId = parentId
|
this.mParentId = parentId
|
||||||
this.mRegisterInfo = registerInfo
|
this.mRegisterInfo = registerInfo
|
||||||
this.mSearchInfo = searchInfo
|
|
||||||
|
|
||||||
database?.let {
|
database?.let {
|
||||||
mEntryId?.let {
|
mEntryId?.let {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
scope = viewModelScope,
|
||||||
|
action = {
|
||||||
// Create an Entry copy to modify from the database entry
|
// Create an Entry copy to modify from the database entry
|
||||||
mEntry = database.getEntryById(it)
|
mEntry = database.getEntryById(it)
|
||||||
// Retrieve the parent
|
// Retrieve the parent
|
||||||
@@ -105,21 +117,24 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
database,
|
database,
|
||||||
entry,
|
entry,
|
||||||
mIsTemplate,
|
mIsTemplate,
|
||||||
registerInfo,
|
registerInfo
|
||||||
searchInfo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ templatesEntry ->
|
onActionComplete = { templatesEntry ->
|
||||||
mEntryId = null
|
mEntryId = null
|
||||||
_templatesEntry.value = templatesEntry
|
_templatesEntry.value = templatesEntry
|
||||||
|
if (templatesEntry?.overwrittenData == true) {
|
||||||
|
mUiState.value = UIState.ShowOverwriteMessage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
mParentId?.let {
|
mParentId?.let {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
scope = viewModelScope,
|
||||||
|
action = {
|
||||||
mParent = database.getGroupById(it)
|
mParent = database.getGroupById(it)
|
||||||
mParent?.let { parentGroup ->
|
mParent?.let { parentGroup ->
|
||||||
mEntry = database.createEntry()?.apply {
|
mEntry = database.createEntry()?.apply {
|
||||||
@@ -145,12 +160,11 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
database,
|
database,
|
||||||
mEntry,
|
mEntry,
|
||||||
mIsTemplate,
|
mIsTemplate,
|
||||||
registerInfo,
|
registerInfo
|
||||||
searchInfo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ templatesEntry ->
|
onActionComplete = { templatesEntry ->
|
||||||
mParentId = null
|
mParentId = null
|
||||||
_templatesEntry.value = templatesEntry
|
_templatesEntry.value = templatesEntry
|
||||||
}
|
}
|
||||||
@@ -159,33 +173,37 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeTemplateEntry(database: ContextualDatabase,
|
private fun decodeTemplateEntry(
|
||||||
entry: Entry?,
|
database: ContextualDatabase,
|
||||||
isTemplate: Boolean,
|
entry: Entry?,
|
||||||
registerInfo: RegisterInfo?,
|
isTemplate: Boolean,
|
||||||
searchInfo: SearchInfo?): TemplatesEntry {
|
registerInfo: RegisterInfo?
|
||||||
|
): TemplatesEntry {
|
||||||
val templates = database.getTemplates(isTemplate)
|
val templates = database.getTemplates(isTemplate)
|
||||||
val entryTemplate = entry?.let { database.getTemplate(it) }
|
val entryTemplate = entry?.let { database.getTemplate(it) }
|
||||||
?: Template.STANDARD
|
?: Template.STANDARD
|
||||||
var entryInfo: EntryInfo? = null
|
var entryInfo: EntryInfo? = null
|
||||||
|
var overwrittenData = false
|
||||||
// Decode the entry / load entry info
|
// Decode the entry / load entry info
|
||||||
entry?.let {
|
entry?.let {
|
||||||
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
|
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
|
||||||
// Load entry info
|
// Load entry info
|
||||||
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
||||||
// Retrieve data from registration
|
// Retrieve data from registration
|
||||||
// TODO only save registration
|
|
||||||
searchInfo?.let { tempSearchInfo ->
|
|
||||||
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
|
|
||||||
}
|
|
||||||
registerInfo?.let { regInfo ->
|
registerInfo?.let { regInfo ->
|
||||||
tempEntryInfo.saveRegisterInfo(database, regInfo)
|
overwrittenData = tempEntryInfo.saveRegisterInfo(database, regInfo)
|
||||||
}
|
}
|
||||||
entryInfo = tempEntryInfo
|
entryInfo = tempEntryInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo)
|
return TemplatesEntry(
|
||||||
|
isTemplate,
|
||||||
|
templates,
|
||||||
|
entryTemplate,
|
||||||
|
entryInfo,
|
||||||
|
overwrittenData
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeTemplate(template: Template) {
|
fun changeTemplate(template: Template) {
|
||||||
@@ -198,44 +216,52 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
|
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun unlockAction() {
|
||||||
|
actionLocked = false
|
||||||
|
}
|
||||||
|
|
||||||
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
|
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
|
||||||
IOActionTask(
|
if (actionLocked.not()) {
|
||||||
{
|
actionLocked = true
|
||||||
removeTempAttachmentsNotCompleted(entryInfo)
|
IOActionTask(
|
||||||
entry?.let { oldEntry ->
|
scope = viewModelScope,
|
||||||
// Create a clone
|
action = {
|
||||||
var newEntry = Entry(oldEntry)
|
removeTempAttachmentsNotCompleted(entryInfo)
|
||||||
|
entry?.let { oldEntry ->
|
||||||
|
// Create a clone
|
||||||
|
var newEntry = Entry(oldEntry)
|
||||||
|
|
||||||
// Build info
|
// Build info
|
||||||
newEntry.setEntryInfo(database, entryInfo)
|
newEntry.setEntryInfo(database, entryInfo)
|
||||||
|
|
||||||
// Encode entry properties for template
|
// Encode entry properties for template
|
||||||
_onTemplateChanged.value?.let { template ->
|
_onTemplateChanged.value?.let { template ->
|
||||||
newEntry =
|
newEntry =
|
||||||
database?.encodeEntryWithTemplateConfiguration(newEntry, template)
|
database?.encodeEntryWithTemplateConfiguration(newEntry, template)
|
||||||
?: newEntry
|
?: newEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete temp attachment if not used
|
// Delete temp attachment if not used
|
||||||
mTempAttachments.forEach { tempAttachmentState ->
|
mTempAttachments.forEach { tempAttachmentState ->
|
||||||
val tempAttachment = tempAttachmentState.attachment
|
val tempAttachment = tempAttachmentState.attachment
|
||||||
database?.attachmentPool?.let { binaryPool ->
|
database?.attachmentPool?.let { binaryPool ->
|
||||||
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
|
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
|
||||||
database.removeAttachmentIfNotUsed(tempAttachment)
|
database.removeAttachmentIfNotUsed(tempAttachment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Return entry to save
|
// Return entry to save
|
||||||
EntrySave(oldEntry, newEntry, parent)
|
EntrySave(oldEntry, newEntry, parent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onActionComplete = { entrySave ->
|
||||||
|
entrySave?.let {
|
||||||
|
_onEntrySaved.value = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
).execute()
|
||||||
{ entrySave ->
|
}
|
||||||
entrySave?.let {
|
|
||||||
_onEntrySaved.value = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).execute()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) {
|
private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) {
|
||||||
@@ -322,10 +348,13 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
|
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TemplatesEntry(val isTemplate: Boolean,
|
data class TemplatesEntry(
|
||||||
val templates: List<Template>,
|
val isTemplate: Boolean,
|
||||||
val defaultTemplate: Template,
|
val templates: List<Template>,
|
||||||
val entryInfo: EntryInfo?)
|
val defaultTemplate: Template,
|
||||||
|
val entryInfo: EntryInfo?,
|
||||||
|
val overwrittenData: Boolean = false
|
||||||
|
)
|
||||||
data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?)
|
data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?)
|
||||||
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
|
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
|
||||||
data class FieldEdition(val oldField: Field?, val newField: Field?)
|
data class FieldEdition(val oldField: Field?, val newField: Field?)
|
||||||
@@ -333,6 +362,11 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
|
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
|
||||||
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
|
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
|
||||||
|
|
||||||
|
sealed class UIState {
|
||||||
|
object Loading: UIState()
|
||||||
|
object ShowOverwriteMessage: UIState()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = EntryEditViewModel::class.java.name
|
private val TAG = EntryEditViewModel::class.java.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/activity_entry_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:filterTouchesWhenObscured="true">
|
android:filterTouchesWhenObscured="true">
|
||||||
|
|||||||
@@ -101,6 +101,13 @@
|
|||||||
android:checked="false"
|
android:checked="false"
|
||||||
style="@style/KeepassDXStyle.Chip.Filter"
|
style="@style/KeepassDXStyle.Chip.Filter"
|
||||||
android:text="@string/entry_password"/>
|
android:text="@string/entry_password"/>
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/search_chip_application_id"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="false"
|
||||||
|
style="@style/KeepassDXStyle.Chip.Filter"
|
||||||
|
android:text="@string/entry_application_id"/>
|
||||||
<com.google.android.material.chip.Chip
|
<com.google.android.material.chip.Chip
|
||||||
android:id="@+id/search_chip_url"
|
android:id="@+id/search_chip_url"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -335,7 +335,7 @@
|
|||||||
<string name="device_unlock_explanation_summary">استخدم إلغاء القفل الجهاز لفتح قاعدة البيانات بسهولة</string>
|
<string name="device_unlock_explanation_summary">استخدم إلغاء القفل الجهاز لفتح قاعدة البيانات بسهولة</string>
|
||||||
<string name="lock_database_show_button_summary">يعرض زر القَفل في الواجهة</string>
|
<string name="lock_database_show_button_summary">يعرض زر القَفل في الواجهة</string>
|
||||||
<string name="lock_database_show_button_title">أظهر زر القفل</string>
|
<string name="lock_database_show_button_title">أظهر زر القفل</string>
|
||||||
<string name="lock_database_back_root_summary">قفل قاعدة البيانات عند النقر على زر الرجوع في الشاشة الرئيسية</string>
|
<string name="lock_database_back_root_summary">اضغط على \"رجوع\" لقفل قاعدة البيانات إذا كنت في الشاشة الجذر لقاعدة البيانات</string>
|
||||||
<string name="lock_database_back_root_title">اضغط على \"رجوع\" للإقفال</string>
|
<string name="lock_database_back_root_title">اضغط على \"رجوع\" للإقفال</string>
|
||||||
<string name="clipboard_explanation_summary">انسخ حقول المدخل باستخدام الحافظة</string>
|
<string name="clipboard_explanation_summary">انسخ حقول المدخل باستخدام الحافظة</string>
|
||||||
<string name="database_opened">قاعدة البيانات مفتوحة</string>
|
<string name="database_opened">قاعدة البيانات مفتوحة</string>
|
||||||
@@ -452,7 +452,6 @@
|
|||||||
<string name="menu_form_filling_settings">ملء النموذج</string>
|
<string name="menu_form_filling_settings">ملء النموذج</string>
|
||||||
<string name="menu_reload_database">أعد تحميل البيانات</string>
|
<string name="menu_reload_database">أعد تحميل البيانات</string>
|
||||||
<string name="menu_external_icon">أيقونة خارجية</string>
|
<string name="menu_external_icon">أيقونة خارجية</string>
|
||||||
<string name="registration_mode">وضع التسجيل</string>
|
|
||||||
<string name="import_app_properties_title">استورد خصائص التطبيق</string>
|
<string name="import_app_properties_title">استورد خصائص التطبيق</string>
|
||||||
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
|
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
|
||||||
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string>
|
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string>
|
||||||
@@ -643,8 +642,8 @@
|
|||||||
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
|
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
|
||||||
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
|
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
|
||||||
<string name="device_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
|
<string name="device_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
|
||||||
<string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string>
|
<string name="menu_appearance_settings_summary">المظاهر والألوان والأيقونات والخطوط والسمات</string>
|
||||||
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
|
<string name="autofill_explanation_summary">اضبط الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
|
||||||
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>
|
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>
|
||||||
<string name="unlock">فتح</string>
|
<string name="unlock">فتح</string>
|
||||||
<string name="menu_app_settings_summary">البحث، القفل، التاريخ، الخصائص</string>
|
<string name="menu_app_settings_summary">البحث، القفل، التاريخ، الخصائص</string>
|
||||||
@@ -692,4 +691,45 @@
|
|||||||
<string name="warning_large_keyfile">لا يُنصح بإضافة ملف مفتاحي كبير، فقد يؤدي هذا إلى منع فتح قاعدة البيانات.</string>
|
<string name="warning_large_keyfile">لا يُنصح بإضافة ملف مفتاحي كبير، فقد يؤدي هذا إلى منع فتح قاعدة البيانات.</string>
|
||||||
<string name="hide_templates_title">أخفِ القوالب</string>
|
<string name="hide_templates_title">أخفِ القوالب</string>
|
||||||
<string name="error_otp_secret_length">يجب أن يتكوّن المفتاح السري من %1$d أحرف على الأقل.</string>
|
<string name="error_otp_secret_length">يجب أن يتكوّن المفتاح السري من %1$d أحرف على الأقل.</string>
|
||||||
|
<string name="entry_application_id">معرّف التطبيق</string>
|
||||||
|
<string name="warning_overwrite_data_title">أتريد الكتابة فوق البيانات الموجودة؟</string>
|
||||||
|
<string name="warning_overwrite_data_description">سيؤدي هذا الإجراء إلى استبدال البيانات الموجودة في الإدخال، ويمكنك استرداد البيانات القديمة إذا كانت المحفوظات مفعلة.</string>
|
||||||
|
<string name="credential_provider">موفّر بيانات الاعتماد</string>
|
||||||
|
<string name="passkeys">مفاتيح المرور</string>
|
||||||
|
<string name="passkeys_explanation_summary">اضبط مفاتيح المرور لتسجيل دخول سريع وآمن بدون كلمة سر</string>
|
||||||
|
<string name="passkeys_preference_title">إعدادات مفاتيح المرور</string>
|
||||||
|
<string name="passkeys_close_database_title">أغلق قاعدة البيانات</string>
|
||||||
|
<string name="passkeys_close_database_summary">أغلق قاعدة البيانات بعد اختيار مفتاح المرور</string>
|
||||||
|
<string name="passkeys_privileged_apps_title">التطبيقات المتميزة</string>
|
||||||
|
<string name="passkeys_privileged_apps_summary">أدر المتصفحات في القائمة المخصّصة للتطبيقات المتميزة</string>
|
||||||
|
<string name="passkeys_privileged_apps_explanation">تحذير: يعمل تطبيق مميز كبوابة لاسترداد أصل الاستيثاق. تأكد من شرعيته لتجنب المشكلات الأمنية.</string>
|
||||||
|
<string name="passkeys_privileged_apps_ask_title">التطبيق غير معروف</string>
|
||||||
|
<string name="passkeys_privileged_apps_ask_message">يحاول %1$s تنفيذ إجراء مفتاح المرور.\n\nأتريد إضافته إلى قائمة التطبيقات المتميزة؟</string>
|
||||||
|
<string name="passkeys_missing_signature_app_ask_title">التوقيع مفقود</string>
|
||||||
|
<string name="passkeys_missing_signature_app_ask_explanation">تحذير: أُنشئ مفتاح المرور من عميل آخر أو حُذف التوقيع. تأكد من أن التطبيق الذي تريد الاستيثاق عليه جزء من نفس الخدمة وأنه شرعي لتجنب المشكلات الأمنية.\nإذا كان التطبيق متصفحًا، فلا تضف توقيعه إلى الإدخال، بل إلى قائمة التطبيقات الموثوقة في الإعدادات.</string>
|
||||||
|
<string name="passkeys_missing_signature_app_ask_message">%1$s غير معروف ويحاول الاستيثاق باستخدام مفتاح مرور موجود.</string>
|
||||||
|
<string name="passkeys_missing_signature_app_ask_question">إضافة توقيع التطبيق إلى إدخال مفتاح المرور؟</string>
|
||||||
|
<string name="passkeys_auto_select_title">تحديد تلقائي</string>
|
||||||
|
<string name="passkeys_auto_select_summary">حدّد تلقائي إذا كان هناك إدخال واحد فقط وقاعدة البيانات مفتوحة، فقط إذا كان التطبيق الطالب متوافقًا</string>
|
||||||
|
<string name="passkeys_backup_eligibility_title">أهلية النسخ الاحتياطي</string>
|
||||||
|
<string name="passkeys_backup_eligibility_summary">تحديد وقت الإنشاء ما إذا كان مسموحًا بنسخ مصدر بيانات اعتماد المفتاح العام احتياطيًا</string>
|
||||||
|
<string name="passkeys_backup_state_title">حالة النسخ الاحتياطي</string>
|
||||||
|
<string name="passkeys_backup_state_summary">أشر إلى أن بيانات الاعتماد مدعومة ومحمية ضد فقدان جهاز واحد</string>
|
||||||
|
<string name="credential_provider_service_subtitle">مفاتيح المرور، موفّر بيانات اعتماد الملء التلقائي</string>
|
||||||
|
<string name="passkey">مفتاح المرور</string>
|
||||||
|
<string name="passkey_service_name">موفّر بيانات اعتماد KeePassDX</string>
|
||||||
|
<string name="passkey_creation_description">احفظ مفتاح المرور في مدخل جديد</string>
|
||||||
|
<string name="passkey_update_description">حدِّث مفتاح المرور في %1$s</string>
|
||||||
|
<string name="passkey_selection_username">لم يُعثر على مفتاح مرور</string>
|
||||||
|
<string name="passkey_selection_description">حدّد مفتاح مرور موجود</string>
|
||||||
|
<string name="passkey_database_username">قاعدة بيانات KeePassDX</string>
|
||||||
|
<string name="passkey_locked_database_description">حدّد لفتح القفل</string>
|
||||||
|
<string name="passkey_username">اسم مستخدم مفتاح المرور</string>
|
||||||
|
<string name="passkey_private_key">المفتاح الخاص لمفتاح المرور</string>
|
||||||
|
<string name="passkey_credential_id">معرّف بيانات مفتاح المرور</string>
|
||||||
|
<string name="passkey_user_handle">معرّف مستخدم مفتاح المرور</string>
|
||||||
|
<string name="passkey_relying_party">الطرف المعتمد لمفتاح المرور</string>
|
||||||
|
<string name="passkey_backup_eligibility">أهلية النسخ الاحتياطي لمفتاح المرور</string>
|
||||||
|
<string name="passkey_backup_state">حالة النسخ الاحتياطي لمفتاح المرور</string>
|
||||||
|
<string name="error_passkey_result">تعذر إرجاع مفتاح المرور</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -485,7 +485,6 @@
|
|||||||
<string name="search_mode">Axtarış modu</string>
|
<string name="search_mode">Axtarış modu</string>
|
||||||
<string name="save_mode">Yadda saxlama modu</string>
|
<string name="save_mode">Yadda saxlama modu</string>
|
||||||
<string name="selection_mode">Seçim modu</string>
|
<string name="selection_mode">Seçim modu</string>
|
||||||
<string name="registration_mode">Qeydiyyat modu</string>
|
|
||||||
<string name="remember_database_locations_title">Məlumat bazalarının yerlərini xatırlayın</string>
|
<string name="remember_database_locations_title">Məlumat bazalarının yerlərini xatırlayın</string>
|
||||||
<string name="remember_database_locations_summary">Məlumat bazalarının harada saxlanıldığını izlə</string>
|
<string name="remember_database_locations_summary">Məlumat bazalarının harada saxlanıldığını izlə</string>
|
||||||
<string name="remember_hardware_key_summary">Aparat-təchizat açarlarının harada istifadə olunduğunu izlə</string>
|
<string name="remember_hardware_key_summary">Aparat-təchizat açarlarının harada istifadə olunduğunu izlə</string>
|
||||||
|
|||||||
@@ -336,7 +336,6 @@
|
|||||||
<string name="menu_keystore_remove_key">Izbrišite ključ za otključavanje uređaja</string>
|
<string name="menu_keystore_remove_key">Izbrišite ključ za otključavanje uređaja</string>
|
||||||
<string name="subdomain_search_summary">Pretražujte veb domene sa ograničenjima poddomena</string>
|
<string name="subdomain_search_summary">Pretražujte veb domene sa ograničenjima poddomena</string>
|
||||||
<string name="export_app_properties_title">Izvezite podešavanja aplikacije</string>
|
<string name="export_app_properties_title">Izvezite podešavanja aplikacije</string>
|
||||||
<string name="registration_mode">Režim registracije</string>
|
|
||||||
<string name="remember_database_locations_title">Zapamtite lokacije baza podataka</string>
|
<string name="remember_database_locations_title">Zapamtite lokacije baza podataka</string>
|
||||||
<string name="remember_hardware_key_title">Zapamtite hardverske ključeve</string>
|
<string name="remember_hardware_key_title">Zapamtite hardverske ključeve</string>
|
||||||
<string name="remember_hardware_key_summary">Vodi evidenciju o korišćenim hardverskim ključevima</string>
|
<string name="remember_hardware_key_summary">Vodi evidenciju o korišćenim hardverskim ključevima</string>
|
||||||
|
|||||||
@@ -296,7 +296,6 @@
|
|||||||
<string name="search_mode">Рэжым пошуку</string>
|
<string name="search_mode">Рэжым пошуку</string>
|
||||||
<string name="save_mode">Рэжым захавання</string>
|
<string name="save_mode">Рэжым захавання</string>
|
||||||
<string name="selection_mode">Рэжым выбару</string>
|
<string name="selection_mode">Рэжым выбару</string>
|
||||||
<string name="registration_mode">Рэжым рэгістрацыі</string>
|
|
||||||
<string name="remember_database_locations_title">Запамінаць размяшчэнне баз дадзеных</string>
|
<string name="remember_database_locations_title">Запамінаць размяшчэнне баз дадзеных</string>
|
||||||
<string name="remember_database_locations_summary">Адсочвае, дзе захоўваюцца базы дадзеных</string>
|
<string name="remember_database_locations_summary">Адсочвае, дзе захоўваюцца базы дадзеных</string>
|
||||||
<string name="remember_keyfile_locations_title">Запамінаць размяшчэнне файлаў ключоў</string>
|
<string name="remember_keyfile_locations_title">Запамінаць размяшчэнне файлаў ключоў</string>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user