mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
244 Commits
4.2.0beta0
...
4.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3b69789bf | ||
|
|
54f2ed9fab | ||
|
|
2fea019b95 | ||
|
|
9ac7ef2d22 | ||
|
|
6d452fa49c | ||
|
|
d99edb6b4d | ||
|
|
cb679f0d59 | ||
|
|
5dd9f75095 | ||
|
|
403021d38b | ||
|
|
fea7b30d6f | ||
|
|
ab5c859db4 | ||
|
|
3fcbc65de0 | ||
|
|
3f1ee6bbea | ||
|
|
37ce2ab781 | ||
|
|
ffaf4a761a | ||
|
|
56b7cc9118 | ||
|
|
987f3f9047 | ||
|
|
3039efc67c | ||
|
|
26daac4637 | ||
|
|
88a93829a9 | ||
|
|
7923a63d36 | ||
|
|
9a5c782d5d | ||
|
|
c39e4ba693 | ||
|
|
7db3d0502f | ||
|
|
d557e8b516 | ||
|
|
d6ae17657b | ||
|
|
3468b0f6f5 | ||
|
|
79777801e8 | ||
|
|
a202f66d48 | ||
|
|
ba58d5d47c | ||
|
|
46685592df | ||
|
|
ba9e2892ef | ||
|
|
a1da3b4fbd | ||
|
|
8bee0ec220 | ||
|
|
aebf6b21de | ||
|
|
0cf9253ea4 | ||
|
|
b63ceb37a4 | ||
|
|
c462dae6f5 | ||
|
|
ddf890b861 | ||
|
|
252eb30b13 | ||
|
|
62ab11cc56 | ||
|
|
e19ad3a8cc | ||
|
|
51fd8a77eb | ||
|
|
5ee0c2eb13 | ||
|
|
6d0ef8265c | ||
|
|
ea69d5acb2 | ||
|
|
1fb9595ec3 | ||
|
|
88e0bd51dc | ||
|
|
67477cc53b | ||
|
|
d2549d61d6 | ||
|
|
d6dc75961b | ||
|
|
f40c83812a | ||
|
|
b29c638d20 | ||
|
|
5bb03c2eef | ||
|
|
a76b1195e5 | ||
|
|
64da26f42c | ||
|
|
ef82552a0f | ||
|
|
d61b27ccd0 | ||
|
|
910ba99056 | ||
|
|
3de2a9acfd | ||
|
|
a48dccf27a | ||
|
|
2a561fb37e | ||
|
|
e27a329ac5 | ||
|
|
8e06a2a7cb | ||
|
|
ace82852af | ||
|
|
73369974b8 | ||
|
|
332eda8a7a | ||
|
|
e5ea1e35aa | ||
|
|
86aae9635a | ||
|
|
db3ccae87d | ||
|
|
4cec26967c | ||
|
|
a0368a4981 | ||
|
|
a1c7fe1e99 | ||
|
|
bf247ddeb7 | ||
|
|
1d2bc0fbfb | ||
|
|
85a12fe4ee | ||
|
|
a443ef996b | ||
|
|
c6995ad403 | ||
|
|
9018807eb8 | ||
|
|
b463106dd5 | ||
|
|
a23d28e1fa | ||
|
|
a0454f42d0 | ||
|
|
1c2ac88f47 | ||
|
|
11eb1bae45 | ||
|
|
089d86165a | ||
|
|
6a7362ad35 | ||
|
|
d2c10e2e4e | ||
|
|
0c20a14e67 | ||
|
|
acccf290de | ||
|
|
6ebe0f78af | ||
|
|
935c09ccd2 | ||
|
|
1eb10ad5bd | ||
|
|
ca4283151e | ||
|
|
8fb98ca4e7 | ||
|
|
be74c9710f | ||
|
|
24fb3c4c30 | ||
|
|
3bdc5fe600 | ||
|
|
c30884d6d0 | ||
|
|
5d26c3bd09 | ||
|
|
02e35cf5b7 | ||
|
|
085aefd2b9 | ||
|
|
1ea5b7a50c | ||
|
|
6cba96dd42 | ||
|
|
46238a76bc | ||
|
|
d5a9b664a1 | ||
|
|
6fdc4504d5 | ||
|
|
8a7c411a35 | ||
|
|
5ff9d5fa2f | ||
|
|
bb0f3c80d3 | ||
|
|
597d9c8274 | ||
|
|
4dd8c06fd2 | ||
|
|
72c66b3cd9 | ||
|
|
c2223afa6f | ||
|
|
d338d1340f | ||
|
|
ed4423666b | ||
|
|
d21fe662ff | ||
|
|
4da1c5bd92 | ||
|
|
18c18605fb | ||
|
|
988cb1a8d0 | ||
|
|
b6e01767e0 | ||
|
|
5414854e9c | ||
|
|
ae7f0732c6 | ||
|
|
d49d33fe3a | ||
|
|
5e7fc2d468 | ||
|
|
0d26e6a870 | ||
|
|
dd92f9ceb6 | ||
|
|
ff9239b9c4 | ||
|
|
319d35e485 | ||
|
|
28e65a4601 | ||
|
|
eb626e5bfe | ||
|
|
e1decf9a23 | ||
|
|
fff0e84b95 | ||
|
|
d73a7004b1 | ||
|
|
f71061e835 | ||
|
|
b2d25cc512 | ||
|
|
4d54b56c1d | ||
|
|
c764c6afff | ||
|
|
87b97a3849 | ||
|
|
5e6db44476 | ||
|
|
8615fa817f | ||
|
|
2f891bacd3 | ||
|
|
0d8a426df4 | ||
|
|
c952eb4415 | ||
|
|
2fd53b9416 | ||
|
|
244ca08890 | ||
|
|
208f1e97d5 | ||
|
|
e4e0628e20 | ||
|
|
f60f31771f | ||
|
|
ff6367bac4 | ||
|
|
540e72812e | ||
|
|
5fe4af8e9d | ||
|
|
ae42ab43b7 | ||
|
|
c463055971 | ||
|
|
1849dca81d | ||
|
|
b3dd3dcfb5 | ||
|
|
fef88ff270 | ||
|
|
f1f7dd1e6c | ||
|
|
409f290e33 | ||
|
|
96c3af097a | ||
|
|
4fe6b2e115 | ||
|
|
cc936b9304 | ||
|
|
e7f2a22583 | ||
|
|
4bf905ecda | ||
|
|
f8d80525d9 | ||
|
|
7ce6092270 | ||
|
|
65857596a6 | ||
|
|
e6253336bd | ||
|
|
e5595a3275 | ||
|
|
366e8bf1d7 | ||
|
|
fa63265599 | ||
|
|
755e0ea9a5 | ||
|
|
a819f2f8a8 | ||
|
|
c92da0a72f | ||
|
|
524963dbd8 | ||
|
|
50b1ac388e | ||
|
|
51c62034df | ||
|
|
e4d0cd89c6 | ||
|
|
bfe50fa985 | ||
|
|
3d798e6585 | ||
|
|
068c59ac98 | ||
|
|
34ec94a0c3 | ||
|
|
576a355342 | ||
|
|
aa19f11699 | ||
|
|
2fb4dff46d | ||
|
|
e6cf3f12a5 | ||
|
|
ca94ce86ba | ||
|
|
dea6b25bb4 | ||
|
|
c48f64d331 | ||
|
|
5e3a504c1f | ||
|
|
b9b7d7b2db | ||
|
|
e085d5d277 | ||
|
|
05336e93a0 | ||
|
|
90b3b56893 | ||
|
|
02c514272e | ||
|
|
989e47ed12 | ||
|
|
1caf132558 | ||
|
|
1b98bd740c | ||
|
|
5adeb5cde0 | ||
|
|
b949d5d861 | ||
|
|
b4264a30a4 | ||
|
|
cf799c0f68 | ||
|
|
97f0ca519b | ||
|
|
cf4047b701 | ||
|
|
40608a3eb5 | ||
|
|
99cb50d031 | ||
|
|
b0d0c35241 | ||
|
|
6044c93a4a | ||
|
|
b544b5d54d | ||
|
|
852378e484 | ||
|
|
711a344860 | ||
|
|
72087c7e5c | ||
|
|
a337de3679 | ||
|
|
75b37f5a9f | ||
|
|
075f54b286 | ||
|
|
e07cbc2e14 | ||
|
|
ac29b7bac7 | ||
|
|
b9129cb941 | ||
|
|
6957fcd81a | ||
|
|
cfe56fc055 | ||
|
|
6f3e065ad1 | ||
|
|
abfa7a3f47 | ||
|
|
dd0d85e54e | ||
|
|
76c20263f7 | ||
|
|
e447388611 | ||
|
|
1bfec67c02 | ||
|
|
45041216d6 | ||
|
|
e075e9018c | ||
|
|
eed304ec40 | ||
|
|
5bcbbac97f | ||
|
|
ea4750fc11 | ||
|
|
5037821529 | ||
|
|
3a4c88f19a | ||
|
|
e960a8e169 | ||
|
|
1d4e1687cf | ||
|
|
033fa95285 | ||
|
|
f17d211fbd | ||
|
|
ae903ad236 | ||
|
|
7c3a15ce79 | ||
|
|
b609d4e182 | ||
|
|
e8ecf28f7c | ||
|
|
3d5adbfc01 | ||
|
|
72bfc50703 | ||
|
|
a60e2e780d | ||
|
|
9210851765 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Bug Report
|
||||
name: Bug report
|
||||
description: Report a bug.
|
||||
labels: ["bug"]
|
||||
body:
|
||||
|
||||
36
CHANGELOG
36
CHANGELOG
@@ -1,10 +1,36 @@
|
||||
KeePassDX(4.2.3)
|
||||
* Fix multiple Passkey selection #2253
|
||||
* Fix database dialog subtitle #2254
|
||||
* Fix save search info if URL present #2255
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.2)
|
||||
* Fix database merge algorithm #2223
|
||||
* Fix save search info #2243
|
||||
* Fix Play Service as privileged app for Passkey Cross Device Authentication #2244
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.1)
|
||||
* Fix Magikeyboard autosearch #2233
|
||||
* Fix database merge #2223
|
||||
* Fix dialog database action #2234
|
||||
* Fix autofill selection #2238 #2235
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.0)
|
||||
* Passkeys management #1421 #2097 (Thx @cali-95)
|
||||
* Confirm usage of passkey #2165
|
||||
* Passkeys management #1421 #2097 (@cali-95)
|
||||
* Confirm usage of passkey #2165 #2124
|
||||
* Dialog to manage missing signature #2152 #2155 #2161 #2160
|
||||
* Capture error #2159
|
||||
* Change Passkey Backup Eligibility & Backup State #2135 #2150
|
||||
* Small fixes #2157 #2164
|
||||
* Capture error #2159 #2215
|
||||
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
|
||||
* Search settings #2112 #2181 #2187 #2204
|
||||
* Autofill refactoring #765 #2196
|
||||
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
|
||||
|
||||
KeePassDX(4.1.9)
|
||||
* Fix landscape UI #2198 #2200 (@chenxiaolong)
|
||||
* Fix start loop and flash screen #2201
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.1.8)
|
||||
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
||||
|
||||
22
README.md
22
README.md
@@ -6,19 +6,21 @@
|
||||
|
||||
### Features
|
||||
|
||||
- Create database files / entries and groups.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||
- **Passkeys** for authentication and **local storage of private keys**.
|
||||
- **Biometric recognition** for fast unlocking (fingerprint / face unlock / …).
|
||||
- **One-Time Password** management (HOTP / TOTP) for two-factor authentication (2FA).
|
||||
- **Autofill** for easy form filling with passwords.
|
||||
- **Magikeyboard** to efficiently fill in any field.
|
||||
- Create **encrypted database files**.
|
||||
- Organisation of credentials by **entry** and in **group** trees.
|
||||
- Allows opening and **copying URI / URL fields quickly**.
|
||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||
- Material design with **themes**.
|
||||
- **Auto-Fill** and Integration.
|
||||
- Field filling **keyboard**.
|
||||
- Dynamic **templates**
|
||||
- Dynamic **templates** for each type of entry.
|
||||
- **History** of each entry.
|
||||
- Precise management of **settings**.
|
||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||
- Material design with **themes**.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||
- Code written in **native languages** (Kotlin / Java / JNI / C).
|
||||
|
||||
KeePassDX is **open source** and **ad-free**.
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 35
|
||||
versionCode = 142
|
||||
versionName = "4.2.0beta02"
|
||||
versionCode = 148
|
||||
versionName = "4.2.3"
|
||||
multiDexEnabled true
|
||||
|
||||
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",
|
||||
"info": {
|
||||
|
||||
@@ -178,18 +178,22 @@
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
||||
android:theme="@style/Theme.Transparent" />
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:excludeFromRecents="true"/>
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:launchMode="singleInstance"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -208,8 +212,8 @@
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
tools:targetApi="upside_down_cake" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||
|
||||
@@ -79,11 +79,13 @@ import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.EnumSet
|
||||
import java.util.UUID
|
||||
|
||||
class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
private var footer: ViewGroup? = null
|
||||
private var container: View? = null
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var appBarLayout: AppBarLayout? = null
|
||||
@@ -123,6 +125,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
private var mBackgroundColor: Int? = null
|
||||
private var mForegroundColor: Int? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -135,6 +139,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
// Get views
|
||||
footer = findViewById(R.id.activity_entry_footer)
|
||||
container = findViewById(R.id.activity_entry_container)
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
appBarLayout = findViewById(R.id.app_bar)
|
||||
@@ -150,8 +155,12 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
setTransparentNavigationBar {
|
||||
// To fix margin with API 27
|
||||
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
||||
container?.applyWindowInsets(EnumSet.of(
|
||||
WindowInsetPosition.TOP_MARGINS,
|
||||
WindowInsetPosition.BOTTOM_MARGINS,
|
||||
WindowInsetPosition.START_MARGINS,
|
||||
WindowInsetPosition.END_MARGINS,
|
||||
))
|
||||
}
|
||||
|
||||
// Empty title
|
||||
@@ -305,11 +314,11 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||
mDatabase?.let { database ->
|
||||
launch(
|
||||
this,
|
||||
database,
|
||||
historySelected.nodeId,
|
||||
historySelected.historyPosition,
|
||||
mEntryActivityResultLauncher
|
||||
activity = this,
|
||||
database = database,
|
||||
entryId = historySelected.nodeId,
|
||||
historyPosition = historySelected.historyPosition,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -323,9 +332,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
return coordinatorLayout
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mEntryViewModel.loadDatabase(database)
|
||||
}
|
||||
|
||||
@@ -471,11 +479,12 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
R.id.menu_edit -> {
|
||||
mDatabase?.let { database ->
|
||||
mMainEntryId?.let { entryId ->
|
||||
EntryEditActivity.launchToUpdate(
|
||||
this,
|
||||
database,
|
||||
entryId,
|
||||
mEntryActivityResultLauncher
|
||||
EntryEditActivity.launch(
|
||||
activity = this,
|
||||
database = database,
|
||||
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||
nodeId = entryId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -513,7 +522,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
// Transit data in previous Activity after an update
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||
setResult(Activity.RESULT_OK, this)
|
||||
setResult(RESULT_OK, this)
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
@@ -527,34 +536,22 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||
|
||||
/**
|
||||
* Open standard Entry activity
|
||||
* Open standard or history Entry activity
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
historyPosition: Int? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
if (database.loaded) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open history Entry activity
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
historyPosition: Int,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||
historyPosition?.let {
|
||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||
}
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,14 +36,14 @@ import android.widget.Spinner
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
@@ -59,10 +59,10 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
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.viewmodels.ColorPickerViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.EnumSet
|
||||
import java.util.UUID
|
||||
|
||||
class EntryEditActivity : DatabaseLockActivity(),
|
||||
@@ -155,8 +157,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
// To ask data lost only one time
|
||||
private var backPressedAlreadyApproved = false
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -181,8 +182,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
|
||||
// To apply fit window with transparency
|
||||
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
|
||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
|
||||
container?.applyWindowInsets(EnumSet.of(
|
||||
WindowInsetPosition.TOP_MARGINS,
|
||||
WindowInsetPosition.BOTTOM_MARGINS,
|
||||
WindowInsetPosition.START_MARGINS,
|
||||
WindowInsetPosition.END_MARGINS,
|
||||
))
|
||||
}
|
||||
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
@@ -206,8 +211,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
mDatabase,
|
||||
entryId,
|
||||
parentId,
|
||||
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
|
||||
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
intent.retrieveRegisterInfo()
|
||||
?: intent.retrieveSearchInfo()?.toRegisterInfo()
|
||||
)
|
||||
|
||||
// To retrieve attachment
|
||||
@@ -374,30 +379,30 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
} ?: run {
|
||||
updateEntry(entrySave.oldEntry, entrySave.newEntry)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't wait for saving if it's to provide autofill
|
||||
mDatabase?.let { database ->
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = intent,
|
||||
defaultAction = {},
|
||||
searchAction = {},
|
||||
saveAction = {},
|
||||
keyboardSelectionAction = {
|
||||
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
||||
},
|
||||
autofillSelectionAction = { _, _ ->
|
||||
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
||||
},
|
||||
autofillRegistrationAction = {
|
||||
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
||||
},
|
||||
passkeySelectionAction = {
|
||||
entryValidatedForPasskeySelection(database, entrySave.newEntry)
|
||||
},
|
||||
passkeyRegistrationAction = {
|
||||
entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mEntryEditViewModel.uiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
EntryEditViewModel.UIState.Loading -> {}
|
||||
EntryEditViewModel.UIState.ShowOverwriteMessage -> {
|
||||
if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
|
||||
AlertDialog.Builder(this@EntryEditActivity)
|
||||
.setTitle(R.string.warning_overwrite_data_title)
|
||||
.setMessage(R.string.warning_overwrite_data_description)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
|
||||
}
|
||||
.create().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,13 +415,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
||||
mAllowOTP = database?.allowOTP == true
|
||||
mEntryEditViewModel.loadDatabase(database)
|
||||
mAllowCustomFields = database.allowEntryCustomFields() == true
|
||||
mAllowOTP = database.allowOTP == true
|
||||
mEntryEditViewModel.loadTemplateEntry(database)
|
||||
mTemplatesSelectorAdapter?.apply {
|
||||
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
||||
iconDrawableFactory = database.iconDrawableFactory
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -427,6 +432,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
mEntryEditViewModel.unlockAction()
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||
@@ -442,23 +448,27 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
searchAction = {
|
||||
// Nothing when search retrieved
|
||||
},
|
||||
saveAction = {
|
||||
entryValidatedForSave(entry)
|
||||
selectionAction = { intentSender, typeMode, searchInfo ->
|
||||
when(typeMode) {
|
||||
TypeMode.DEFAULT -> {}
|
||||
TypeMode.MAGIKEYBOARD ->
|
||||
entryValidatedForKeyboardSelection(database, entry)
|
||||
TypeMode.PASSKEY ->
|
||||
entryValidatedForPasskey(database, entry)
|
||||
TypeMode.AUTOFILL ->
|
||||
entryValidatedForAutofill(database, entry)
|
||||
}
|
||||
},
|
||||
keyboardSelectionAction = {
|
||||
entryValidatedForKeyboardSelection(database, entry)
|
||||
},
|
||||
autofillSelectionAction = { _, _ ->
|
||||
entryValidatedForAutofillSelection(database, entry)
|
||||
},
|
||||
autofillRegistrationAction = {
|
||||
entryValidatedForAutofillRegistration(entry)
|
||||
},
|
||||
passkeySelectionAction = {
|
||||
entryValidatedForPasskeySelection(database, entry)
|
||||
},
|
||||
passkeyRegistrationAction = {
|
||||
entryValidatedForPasskeyRegistration(database, entry)
|
||||
registrationAction = { _, typeMode, _ ->
|
||||
when(typeMode) {
|
||||
TypeMode.DEFAULT ->
|
||||
entryValidatedForSave(entry)
|
||||
TypeMode.MAGIKEYBOARD -> {}
|
||||
TypeMode.PASSKEY ->
|
||||
entryValidatedForPasskey(database, entry)
|
||||
TypeMode.AUTOFILL ->
|
||||
entryValidatedForAutofill(database, entry)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -477,46 +487,26 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
|
||||
// Populate Magikeyboard with entry
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entry.getEntryInfo(database)
|
||||
// Build Magikeyboard response with the entry selected
|
||||
this.buildSpecialModeResponseAndSetResult(
|
||||
entryInfo = entry.getEntryInfo(database),
|
||||
extras = buildEntryResult(entry)
|
||||
)
|
||||
onValidateSpecialMode()
|
||||
// Don't keep activity history for entry edition
|
||||
finishForEntryResult(entry)
|
||||
}
|
||||
|
||||
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
|
||||
private fun entryValidatedForAutofill(database: ContextualDatabase, entry: Entry) {
|
||||
// Build Autofill response with the entry selected
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
|
||||
database,
|
||||
entry.getEntryInfo(database))
|
||||
}
|
||||
onValidateSpecialMode()
|
||||
}
|
||||
|
||||
private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
this.buildPasskeyResponseAndSetResult(
|
||||
entryInfo = entry.getEntryInfo(database)
|
||||
this.buildSpecialModeResponseAndSetResult(
|
||||
entryInfo = entry.getEntryInfo(database),
|
||||
extras = buildEntryResult(entry)
|
||||
)
|
||||
}
|
||||
onValidateSpecialMode()
|
||||
}
|
||||
|
||||
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
||||
//if (isIntentSender()) {
|
||||
// TODO Autofill Callback #765
|
||||
//}
|
||||
onValidateSpecialMode()
|
||||
if (!isIntentSender()) {
|
||||
finishForEntryResult(entry)
|
||||
}
|
||||
}
|
||||
|
||||
private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) {
|
||||
private fun entryValidatedForPasskey(database: ContextualDatabase, entry: Entry) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
this.buildPasskeyResponseAndSetResult(
|
||||
entryInfo = entry.getEntryInfo(database),
|
||||
@@ -757,13 +747,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
private fun onApprovedBackPressed(approved: () -> Unit) {
|
||||
if (!backPressedAlreadyApproved) {
|
||||
if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.discard_changes)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.discard) { _, _ ->
|
||||
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||
backPressedAlreadyApproved = true
|
||||
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||
approved.invoke()
|
||||
}.create().show()
|
||||
} else {
|
||||
@@ -783,7 +773,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
val bundle = buildEntryResult(entry)
|
||||
val intentEntry = Intent()
|
||||
intentEntry.putExtras(bundle)
|
||||
setResult(Activity.RESULT_OK, intentEntry)
|
||||
setResult(RESULT_OK, intentEntry)
|
||||
super.finish()
|
||||
} catch (e: Exception) {
|
||||
// Exception when parcelable can't be done
|
||||
@@ -791,6 +781,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
enum class RegistrationType {
|
||||
UPDATE, CREATE
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = EntryEditActivity::class.java.name
|
||||
@@ -800,23 +794,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
const val KEY_PARENT = "parent"
|
||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||
|
||||
fun registerForEntryResult(fragment: Fragment,
|
||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
||||
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
entryAddedOrUpdatedListener.invoke(
|
||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
)
|
||||
} else {
|
||||
entryAddedOrUpdatedListener.invoke(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun registerForEntryResult(activity: FragmentActivity,
|
||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
||||
fun registerForEntryResult(
|
||||
activity: FragmentActivity,
|
||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit
|
||||
): ActivityResultLauncher<Intent> {
|
||||
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
entryAddedOrUpdatedListener.invoke(
|
||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
)
|
||||
@@ -827,176 +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,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
registrationType: RegistrationType,
|
||||
nodeId: NodeId<*>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
when (registrationType) {
|
||||
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||
}
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new entry in an existent group
|
||||
* Launch EntryEditActivity to add a new entry in special selection
|
||||
*/
|
||||
fun launchToCreate(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
groupId: NodeId<*>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launchToUpdateForSave(context: Context,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
searchInfo: SearchInfo) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||
context,
|
||||
intent,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launchToCreateForSave(context: Context,
|
||||
database: ContextualDatabase,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo) {
|
||||
fun launchForSelection(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
typeMode: TypeMode,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||
) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||
context,
|
||||
intent,
|
||||
searchInfo
|
||||
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||
context = context,
|
||||
intent = intent,
|
||||
typeMode = typeMode,
|
||||
searchInfo = searchInfo,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new entry in keyboard selection
|
||||
* Launch EntryEditActivity to update an updated entry or register a new entry (from autofill)
|
||||
*/
|
||||
fun launchForKeyboardSelectionResult(context: Context,
|
||||
database: ContextualDatabase,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
fun launchForRegistration(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
nodeId: NodeId<*>,
|
||||
registerInfo: RegisterInfo? = null,
|
||||
typeMode: TypeMode,
|
||||
registrationType: RegistrationType,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||
) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||
context,
|
||||
intent,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new entry in autofill selection
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
database: ContextualDatabase,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new passkey entry
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
fun launchForPasskeySelectionResult(context: Context,
|
||||
database: ContextualDatabase,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||
context,
|
||||
intent,
|
||||
activityResultLauncher,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to register an updated entry (from autofill)
|
||||
*/
|
||||
fun launchToUpdateForRegistration(context: Context,
|
||||
database: ContextualDatabase,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
entryId: NodeId<UUID>,
|
||||
registerInfo: RegisterInfo?,
|
||||
typeMode: TypeMode) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context,
|
||||
activityResultLauncher,
|
||||
intent,
|
||||
registerInfo,
|
||||
typeMode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to register a new entry (from autofill)
|
||||
*/
|
||||
fun launchToCreateForRegistration(context: Context,
|
||||
database: ContextualDatabase,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
groupId: NodeId<*>,
|
||||
registerInfo: RegisterInfo? = null,
|
||||
typeMode: TypeMode) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
when (registrationType) {
|
||||
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||
}
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context,
|
||||
activityResultLauncher,
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -33,8 +32,6 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
@@ -50,10 +47,8 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
@@ -99,8 +94,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.buildActivityResultLauncher()
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -131,7 +125,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
launchMainCredentialActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||
@@ -160,7 +154,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||
launchPasswordActivity(
|
||||
launchMainCredentialActivity(
|
||||
databaseFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||
@@ -179,7 +173,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
// Load default database the first time
|
||||
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
|
||||
launchPasswordActivityWithPath(databaseFileUri)
|
||||
launchMainCredentialActivityWithPath(databaseFileUri)
|
||||
}
|
||||
|
||||
// Retrieve the database URI provided by file manager after an orientation change
|
||||
@@ -224,11 +218,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
if (database != null) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -236,8 +227,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Update list
|
||||
when (actionTask) {
|
||||
@@ -263,13 +252,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
database,
|
||||
false
|
||||
)
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,17 +276,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
|
||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||
MainCredentialActivity.launch(this,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
{ exception ->
|
||||
fileNoFoundAction(exception)
|
||||
private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = this.intent,
|
||||
defaultAction = {
|
||||
MainCredentialActivity.launch(
|
||||
activity = this,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey
|
||||
)
|
||||
},
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mCredentialActivityResultLauncher)
|
||||
searchAction = { searchInfo ->
|
||||
MainCredentialActivity.launchForSearchResult(
|
||||
activity = this,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
selectionAction = { intentSenderMode, typeMode, searchInfo ->
|
||||
MainCredentialActivity.launchForSelection(
|
||||
activity = this,
|
||||
activityResultLauncher = if (intentSenderMode)
|
||||
mCredentialActivityResultLauncher else null,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
typeMode = typeMode,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
registrationAction = { intentSenderMode, typeMode, registerInfo ->
|
||||
MainCredentialActivity.launchForRegistration(
|
||||
activity = this,
|
||||
activityResultLauncher = if (intentSenderMode)
|
||||
mCredentialActivityResultLauncher else null,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
typeMode = typeMode,
|
||||
registerInfo = registerInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||
@@ -307,12 +337,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mCredentialActivityResultLauncher)
|
||||
mCredentialActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
launchPasswordActivity(databaseUri, null, null)
|
||||
private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
|
||||
launchMainCredentialActivity(databaseUri, null, null)
|
||||
// Delete flickering for kitkat <=
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
@@ -336,10 +367,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
mDatabase?.let { database ->
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
|
||||
// Show recent files if allowed
|
||||
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||
databaseFilesViewModel.loadListOfDatabases()
|
||||
@@ -358,7 +385,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
try {
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
// Create the new database
|
||||
createDatabase(databaseUri, mainCredential)
|
||||
mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val error = getString(R.string.error_create_database_file)
|
||||
@@ -442,71 +469,35 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSearchResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
fun launchForSearch(
|
||||
context: Context,
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Save Launch
|
||||
* Selection Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSaveResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForKeyboardSelectionResult(activity: Activity,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Autofill Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Passkey Launch
|
||||
* -------------------------
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
fun launchForPasskeySelectionResult(activity: Activity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||
activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
activityResultLauncher,
|
||||
searchInfo
|
||||
fun launchForSelection(
|
||||
context: Context,
|
||||
typeMode: TypeMode,
|
||||
searchInfo: SearchInfo? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo = searchInfo,
|
||||
typeMode = typeMode,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
|
||||
@@ -515,16 +506,18 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
registerInfo: RegisterInfo? = null,
|
||||
typeMode: TypeMode) {
|
||||
fun launchForRegistration(
|
||||
context: Context,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context,
|
||||
activityResultLauncher,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo,
|
||||
typeMode
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo = registerInfo,
|
||||
typeMode = typeMode,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,8 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -174,10 +176,10 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
if (database?.allowCustomIcons == true) {
|
||||
if (database.allowCustomIcons) {
|
||||
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
} else {
|
||||
uploadButton.visibility = View.GONE
|
||||
|
||||
@@ -45,6 +45,8 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
private lateinit var imageView: ImageView
|
||||
private lateinit var progressView: View
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -101,7 +103,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
try {
|
||||
@@ -119,18 +121,16 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
resources.displayMetrics.heightPixels * 2
|
||||
)
|
||||
|
||||
database?.let { database ->
|
||||
BinaryDatabaseManager.loadBitmap(
|
||||
database,
|
||||
attachment.binaryData,
|
||||
mImagePreviewMaxWidth
|
||||
) { bitmapLoaded ->
|
||||
if (bitmapLoaded == null) {
|
||||
finish()
|
||||
} else {
|
||||
progressView.visibility = View.GONE
|
||||
imageView.setImageBitmap(bitmapLoaded)
|
||||
}
|
||||
BinaryDatabaseManager.loadBitmap(
|
||||
database,
|
||||
attachment.binaryData,
|
||||
mImagePreviewMaxWidth
|
||||
) { bitmapLoaded ->
|
||||
if (bitmapLoaded == null) {
|
||||
finish()
|
||||
} else {
|
||||
progressView.visibility = View.GONE
|
||||
imageView.setImageBitmap(bitmapLoaded)
|
||||
}
|
||||
}
|
||||
} ?: finish()
|
||||
|
||||
@@ -28,6 +28,8 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
|
||||
private lateinit var validationButton: View
|
||||
private var lockView: View? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -36,8 +36,6 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
@@ -56,10 +54,8 @@ import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
@@ -128,8 +124,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
private var mReadOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
|
||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.buildActivityResultLauncher()
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -310,26 +305,20 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
|
||||
mDatabase?.let { database ->
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
if (database != null) {
|
||||
// Trying to load another database
|
||||
if (mDatabaseFileUri != null
|
||||
&& database.fileUri != null
|
||||
&& mDatabaseFileUri != database.fileUri) {
|
||||
Toast.makeText(this,
|
||||
R.string.warning_database_already_opened,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
launchGroupActivityIfLoaded(database)
|
||||
// Trying to load another database
|
||||
if (mDatabaseFileUri != null
|
||||
&& database.fileUri != null
|
||||
&& mDatabaseFileUri != database.fileUri) {
|
||||
Toast.makeText(this,
|
||||
R.string.warning_database_already_opened,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -514,10 +503,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||
// Consume the intent extra password
|
||||
intent.removeExtra(KEY_PASSWORD)
|
||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||
if (password != null) {
|
||||
mainCredentialView?.populatePasswordTextView(password)
|
||||
}
|
||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
|
||||
if (launchImmediately) {
|
||||
loadDatabase()
|
||||
} else {
|
||||
@@ -572,10 +562,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
if (mReadOnly && (
|
||||
mSpecialMode == SpecialMode.SAVE
|
||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||
) {
|
||||
if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
|
||||
Log.e(TAG, getString(R.string.error_save_read_only))
|
||||
Snackbar.make(coordinatorLayout,
|
||||
R.string.error_save_read_only,
|
||||
@@ -599,7 +586,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUUID: Boolean) {
|
||||
loadDatabase(
|
||||
mDatabaseViewModel.loadDatabase(
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
@@ -752,11 +739,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
private fun buildAndLaunchIntent(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
intentBuildLauncher: (Intent) -> Unit
|
||||
) {
|
||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||
if (keyFile != null)
|
||||
@@ -773,10 +762,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
@@ -789,245 +780,73 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSearchResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
fun launchForSearchResult(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Save Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSaveResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForKeyboardResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Autofill Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Passkey Launch
|
||||
* -------------------------
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForPasskeyResult(activity: Activity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
activityResultLauncher,
|
||||
searchInfo
|
||||
context = activity,
|
||||
intent = intent,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Registration Launch
|
||||
* Selection Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSelection(
|
||||
activity: Activity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo?
|
||||
searchInfo: SearchInfo?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||
context = activity,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
intent = intent,
|
||||
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 {
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = activity.intent,
|
||||
defaultAction = {
|
||||
launch(
|
||||
activity = activity,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey
|
||||
)
|
||||
},
|
||||
searchAction = { searchInfo ->
|
||||
launchForSearchResult(
|
||||
activity = activity,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
saveAction = { searchInfo ->
|
||||
launchForSaveResult(
|
||||
activity = activity,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
keyboardSelectionAction = { searchInfo ->
|
||||
launchForKeyboardResult(
|
||||
activity = activity,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
launchForAutofillResult(
|
||||
activity = activity,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
autofillComponent = autofillComponent,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
autofillRegistrationAction = { registerInfo ->
|
||||
launchForRegistration(
|
||||
activity = activity,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
typeMode = TypeMode.AUTOFILL,
|
||||
registerInfo = registerInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
passkeySelectionAction = { searchInfo ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
launchForPasskeyResult(
|
||||
activity = activity,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
passkeyRegistrationAction = { registerInfo ->
|
||||
launchForRegistration(
|
||||
activity = activity,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
typeMode = TypeMode.PASSKEY,
|
||||
registerInfo = registerInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForRegistration(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context = activity,
|
||||
intent = intent,
|
||||
typeMode = typeMode,
|
||||
registerInfo = registerInfo,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
actionDatabaseListener?.validateDatabaseChanged()
|
||||
actionDatabaseListener?.onDatabaseChangeValidated()
|
||||
}
|
||||
return builder.create()
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
|
||||
interface ActionDatabaseChangedListener {
|
||||
fun validateDatabaseChanged()
|
||||
fun onDatabaseChangeValidated()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -86,9 +86,10 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
|
||||
|
||||
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnly: Boolean
|
||||
fun getInstance(
|
||||
oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnly: Boolean
|
||||
)
|
||||
: DatabaseChangedDialogFragment {
|
||||
val fragment = DatabaseChangedDialogFragment()
|
||||
|
||||
@@ -5,6 +5,9 @@ import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
@@ -12,23 +15,40 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabase: ContextualDatabase? = null
|
||||
private val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mDatabaseViewModel.database.observe(this) { database ->
|
||||
this.mDatabase = database
|
||||
resetAppTimeoutOnTouchOrFocus()
|
||||
onDatabaseRetrieved(database)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +72,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
resetAppTimeoutOnTouchOrFocus()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
|
||||
|
||||
@@ -62,14 +62,14 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
||||
private lateinit var uuidContainerView: ViewGroup
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||
|
||||
if (database?.allowCustomSearchableGroup() == true) {
|
||||
if (database.allowCustomSearchableGroup()) {
|
||||
searchableLabelView.visibility = View.VISIBLE
|
||||
searchableView.visibility = View.VISIBLE
|
||||
} else {
|
||||
|
||||
@@ -112,32 +112,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||
|
||||
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
|
||||
searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (database?.allowAutoType() == true) {
|
||||
if (database.allowAutoType()) {
|
||||
autoTypeContainerView.visibility = View.VISIBLE
|
||||
} else {
|
||||
autoTypeContainerView.visibility = View.GONE
|
||||
}
|
||||
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||
tagsCompletionView.apply {
|
||||
threshold = 1
|
||||
setAdapter(tagsAdapter)
|
||||
}
|
||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
||||
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mCustomIcon: IconImageCustom? = null
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
|
||||
}
|
||||
mCustomIcon?.let { customIcon ->
|
||||
populateViewsWithCustomIcon(customIcon)
|
||||
|
||||
@@ -35,9 +35,9 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
@@ -258,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
} else if (!error
|
||||
&& hardwareKey != null
|
||||
&& !HardwareKeyActivity.isHardwareKeyAvailable(
|
||||
requireActivity(), hardwareKey, false)
|
||||
&& !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
|
||||
) {
|
||||
// show hardware driver dialog if required
|
||||
error = true
|
||||
|
||||
@@ -4,36 +4,59 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
protected var mDatabase: ContextualDatabase? = null
|
||||
protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
protected val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
||||
if (mDatabase == null || mDatabase != database) {
|
||||
this.mDatabase = database
|
||||
onDatabaseRetrieved(database)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
|
||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
||||
context?.let {
|
||||
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
|
||||
view?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||
context = it,
|
||||
databaseLoaded = mDatabase?.loaded
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +67,4 @@ abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||
) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
|
||||
protected fun buildNewBinaryAttachment(): BinaryData? {
|
||||
return mDatabase?.buildNewBinaryAttachment()
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
val attachmentToUploadUri = it.attachmentToUploadUri
|
||||
val fileName = it.fileName
|
||||
|
||||
buildNewBinaryAttachment()?.let { binaryAttachment ->
|
||||
mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
|
||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||
// Ask to replace the current attachment
|
||||
if ((!mAllowMultipleAttachments
|
||||
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
|
||||
templateView.populateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
|
||||
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
|
||||
mAllowMultipleAttachments = database.allowMultipleAttachments == true
|
||||
|
||||
attachmentsAdapter?.database = database
|
||||
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||
tagsCompletionView.apply {
|
||||
threshold = 1
|
||||
setAdapter(tagsAdapter)
|
||||
}
|
||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
||||
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||
|
||||
@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
context?.let { context ->
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||
attachmentsAdapter?.database = database
|
||||
|
||||
@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
@@ -154,46 +154,44 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
context?.let { context ->
|
||||
database?.let { database ->
|
||||
mAdapter = NodesAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
listActionNodes.remove(node)
|
||||
} else {
|
||||
// Add selected item if not already selected
|
||||
listActionNodes.add(node)
|
||||
}
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
mAdapter = NodesAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
listActionNodes.remove(node)
|
||||
} else {
|
||||
nodeClickListener?.onNodeClick(database, node)
|
||||
// Add selected item if not already selected
|
||||
listActionNodes.add(node)
|
||||
}
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
} else {
|
||||
nodeClickListener?.onNodeClick(database, node)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
|
||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
// Select the first item after a long click
|
||||
if (!listActionNodes.contains(node))
|
||||
listActionNodes.add(node)
|
||||
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
|
||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
// Select the first item after a long click
|
||||
if (!listActionNodes.contains(node))
|
||||
listActionNodes.add(node)
|
||||
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
activity?.hideKeyboard()
|
||||
}
|
||||
return true
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
activity?.hideKeyboard()
|
||||
}
|
||||
})
|
||||
}
|
||||
mNodesRecyclerView?.adapter = mAdapter
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
mNodesRecyclerView?.adapter = mAdapter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +246,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
||||
activity?.intent?.let {
|
||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||
specialMode = it.retrieveSpecialMode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,9 +297,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
}
|
||||
|
||||
private fun containsRecycleBin(nodes: List<Node>): Boolean {
|
||||
return mDatabase?.isRecycleBinEnabled == true
|
||||
&& nodes.any { it == mDatabase?.recycleBin }
|
||||
private fun containsRecycleBin(database: ContextualDatabase?, nodes: List<Node>): Boolean {
|
||||
return database?.isRecycleBinEnabled == true
|
||||
&& nodes.any { it == database.recycleBin }
|
||||
}
|
||||
|
||||
fun actionNodesCallback(database: ContextualDatabase,
|
||||
@@ -328,7 +326,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
// Open and Edit for a single item
|
||||
if (nodes.size == 1) {
|
||||
// Edition
|
||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
} else {
|
||||
@@ -348,7 +346,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||
menu?.removeItem(R.id.menu_delete)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val populateList = launch {
|
||||
|
||||
@@ -48,9 +48,9 @@ class IconPickerFragment : DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||
if (database?.allowCustomIcons == true) 2 else 1)
|
||||
if (database.allowCustomIcons) 2 else 1)
|
||||
viewPager.adapter = iconPickerPagerAdapter
|
||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||
tab.text = when (position) {
|
||||
|
||||
@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
|
||||
@@ -293,20 +293,22 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
||||
private fun generatePassword() {
|
||||
var password = ""
|
||||
try {
|
||||
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
|
||||
uppercaseCompound.isChecked,
|
||||
lowercaseCompound.isChecked,
|
||||
digitsCompound.isChecked,
|
||||
minusCompound.isChecked,
|
||||
underlineCompound.isChecked,
|
||||
spaceCompound.isChecked,
|
||||
specialsCompound.isChecked,
|
||||
bracketsCompound.isChecked,
|
||||
extendedCompound.isChecked,
|
||||
getConsiderChars(),
|
||||
getIgnoreChars(),
|
||||
atLeastOneCompound.isChecked,
|
||||
excludeAmbiguousCompound.isChecked)
|
||||
password = PasswordGenerator(resources).generatePassword(
|
||||
length = getPasswordLength(),
|
||||
upperCase = uppercaseCompound.isChecked,
|
||||
lowerCase = lowercaseCompound.isChecked,
|
||||
digits = digitsCompound.isChecked,
|
||||
minus = minusCompound.isChecked,
|
||||
underline = underlineCompound.isChecked,
|
||||
space = spaceCompound.isChecked,
|
||||
specials = specialsCompound.isChecked,
|
||||
brackets = bracketsCompound.isChecked,
|
||||
extended = extendedCompound.isChecked,
|
||||
considerChars = getConsiderChars(),
|
||||
ignoreChars = getIgnoreChars(),
|
||||
atLeastOneFromEach = atLeastOneCompound.isChecked,
|
||||
excludeAmbiguousChar = excludeAmbiguousCompound.isChecked
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to generate a password", e)
|
||||
}
|
||||
@@ -318,7 +320,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
|
||||
@@ -1,104 +1,307 @@
|
||||
package com.kunzisoft.keepass.activities.legacy
|
||||
|
||||
import android.net.Uri
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
|
||||
|
||||
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||
protected var mDatabase: ContextualDatabase? = null
|
||||
protected val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
private val progressTaskViewModel: ProgressTaskViewModel by viewModels()
|
||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||
|
||||
private val mActionDatabaseListener =
|
||||
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||
override fun onDatabaseChangeValidated() {
|
||||
mDatabaseViewModel.onDatabaseChangeValidated()
|
||||
}
|
||||
}
|
||||
|
||||
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { _ ->
|
||||
// Whether or not the user has accepted, the service can be started,
|
||||
// There just won't be any notification if it's not allowed.
|
||||
tempServiceParameters.removeFirstOrNull()?.let {
|
||||
startDatabaseService(it.first, it.second)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful to only waiting for the activity result and prevent any parallel action
|
||||
*/
|
||||
var credentialResultLaunched = false
|
||||
|
||||
/**
|
||||
* Utility activity result launcher,
|
||||
* Used recursively, close each activity with return data
|
||||
*/
|
||||
protected var mCredentialActivityResultLauncher: CredentialActivityResultLauncher =
|
||||
CredentialActivityResultLauncher(
|
||||
registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
setActivityResult(
|
||||
lockDatabase = false,
|
||||
resultCode = it.resultCode,
|
||||
data = it.data
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Custom ActivityResultLauncher to manage the database action
|
||||
*/
|
||||
protected inner class CredentialActivityResultLauncher(
|
||||
val builder: ActivityResultLauncher<Intent>
|
||||
) : ActivityResultLauncher<Intent>() {
|
||||
|
||||
override fun launch(
|
||||
input: Intent?,
|
||||
options: ActivityOptionsCompat?
|
||||
) {
|
||||
credentialResultLaunched = true
|
||||
builder.launch(input, options)
|
||||
}
|
||||
|
||||
override fun unregister() {
|
||||
builder.unregister()
|
||||
}
|
||||
|
||||
override fun getContract(): ActivityResultContract<Intent?, *> {
|
||||
return builder.getContract()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(CREDENTIAL_RESULT_LAUNCHER_KEY)
|
||||
) {
|
||||
credentialResultLaunched = savedInstanceState.getBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY)
|
||||
}
|
||||
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
val databaseWasReloaded = database?.wasReloaded == true
|
||||
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
|
||||
finish()
|
||||
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
|
||||
database?.wasReloaded = false
|
||||
onDatabaseRetrieved(database)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
if (credentialResultLaunched.not()) {
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.Wait -> {}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
|
||||
if (finishActivityIfReloadRequested()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> {
|
||||
if (manageDatabaseInfo()) {
|
||||
showDatabaseChangedDialog(
|
||||
uiState.previousDatabaseInfo,
|
||||
uiState.newDatabaseInfo,
|
||||
uiState.readOnlyDatabase
|
||||
)
|
||||
}
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
|
||||
startDatabasePermissionService(
|
||||
uiState.bundle,
|
||||
uiState.actionTask
|
||||
)
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
|
||||
progressTaskViewModel.show(uiState.progressMessage)
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
|
||||
progressTaskViewModel.show(uiState.progressMessage)
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
|
||||
progressTaskViewModel.hide()
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
progressTaskViewModel.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
|
||||
onDatabaseActionFinished(database, actionTask, result)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
progressTaskViewModel.progressTaskState.collect { state ->
|
||||
when (state) {
|
||||
is ProgressTaskViewModel.ProgressTaskState.Show ->
|
||||
startDialog()
|
||||
is ProgressTaskViewModel.ProgressTaskState.Hide ->
|
||||
stopDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
if (credentialResultLaunched.not()) {
|
||||
// Nullable function
|
||||
onUnknownDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun showDatabaseDialog(): Boolean {
|
||||
return true
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY, credentialResultLaunched)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
mDatabaseTaskProvider?.destroy()
|
||||
mDatabaseTaskProvider = null
|
||||
mDatabase = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
mDatabase = database
|
||||
mDatabaseViewModel.defineDatabase(database)
|
||||
/**
|
||||
* Nullable function to retrieve a database
|
||||
*/
|
||||
open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
// optional method implementation
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// optional method implementation
|
||||
}
|
||||
|
||||
open fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
mDatabaseViewModel.onActionFinished(database, actionTask, result)
|
||||
// optional method implementation
|
||||
}
|
||||
|
||||
fun createDatabase(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
startDatabaseService(bundle, actionTask)
|
||||
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
) {
|
||||
// it's not the first time, so the user deliberately chooses not to display the notification
|
||||
startDatabaseService(bundle, actionTask)
|
||||
} else {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.warning_database_notification_permission)
|
||||
.setNegativeButton(R.string.later) { _, _ ->
|
||||
// Refuses the notification, so start the service
|
||||
startDatabaseService(bundle, actionTask)
|
||||
}
|
||||
.setPositiveButton(R.string.ask) { _, _ ->
|
||||
// Save the temp parameters to ask the permission
|
||||
tempServiceParameters.add(Pair(bundle, actionTask))
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}.create().show()
|
||||
}
|
||||
} else {
|
||||
startDatabaseService(bundle, actionTask)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDatabaseChangedDialog(
|
||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnlyDatabase: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
||||
lifecycleScope.launch {
|
||||
if (databaseChangedDialogFragment == null) {
|
||||
databaseChangedDialogFragment = supportFragmentManager
|
||||
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||
previousDatabaseInfo,
|
||||
newDatabaseInfo,
|
||||
readOnlyDatabase
|
||||
)
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
databaseChangedDialogFragment?.show(
|
||||
supportFragmentManager,
|
||||
DATABASE_CHANGED_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDatabase(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUuid: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider?.startDatabaseLoad(
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
fixDuplicateUuid
|
||||
)
|
||||
private fun startDialog() {
|
||||
lifecycleScope.launch {
|
||||
if (showDatabaseDialog()) {
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = supportFragmentManager
|
||||
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||
progressTaskDialogFragment?.show(
|
||||
supportFragmentManager,
|
||||
PROGRESS_TASK_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun closeDatabase() {
|
||||
mDatabase?.clearAndClose(this.getBinaryDir())
|
||||
private fun stopDialog() {
|
||||
progressTaskDialogFragment?.dismissAllowingStateLoss()
|
||||
progressTaskDialogFragment = null
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
mDatabaseTaskProvider?.registerProgressTask()
|
||||
protected open fun showDatabaseDialog(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||
super.onPause()
|
||||
|
||||
companion object {
|
||||
const val CREDENTIAL_RESULT_LAUNCHER_KEY = "com.kunzisoft.keepass.CREDENTIAL_RESULT_LAUNCHER_KEY"
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
@@ -87,128 +87,44 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
deleteDatabaseNodes(nodes)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDatabase.observe(this) { save ->
|
||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
||||
}
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveName.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDescription.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveColor.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveCompression.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.removeUnlinkData.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveRecycleBin.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveEncryption.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveKeyDerivation.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveIterations.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveMemoryUsage.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveParallelism.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mExitLock = false
|
||||
}
|
||||
|
||||
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// End activity if database not loaded
|
||||
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
|
||||
if (database.loaded.not())
|
||||
finish()
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout,
|
||||
// view is not necessary loaded so retry later in resume
|
||||
viewToInvalidateTimeout()
|
||||
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
|
||||
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
|
||||
|
||||
database?.let {
|
||||
// check timeout
|
||||
if (mTimeoutEnable) {
|
||||
if (mLockReceiver == null) {
|
||||
mLockReceiver = LockReceiver {
|
||||
mDatabase = null
|
||||
closeDatabase(database)
|
||||
mExitLock = true
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
}
|
||||
registerLockReceiver(mLockReceiver)
|
||||
// check timeout
|
||||
if (mTimeoutEnable) {
|
||||
if (mLockReceiver == null) {
|
||||
mLockReceiver = LockReceiver {
|
||||
closeDatabase(database)
|
||||
mExitLock = true
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
}
|
||||
|
||||
// After the first creation
|
||||
// or If simply swipe with another application
|
||||
// If the time is out -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
// If onCreate already record time
|
||||
if (!mExitLock)
|
||||
TimeoutHelper.recordTime(this, database.loaded)
|
||||
registerLockReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
mDatabaseReadOnly = database.isReadOnly
|
||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||
|
||||
checkRegister()
|
||||
// After the first creation
|
||||
// or If simply swipe with another application
|
||||
// If the time is out -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
// If onCreate already record time
|
||||
if (!mExitLock)
|
||||
TimeoutHelper.recordTime(this, database.loaded)
|
||||
}
|
||||
|
||||
mDatabaseReadOnly = database.isReadOnly
|
||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||
|
||||
checkRegister()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
@@ -227,7 +143,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||
@@ -249,24 +164,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
databaseUri: Uri?,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
assignDatabasePassword(databaseUri, mainCredential)
|
||||
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
|
||||
}
|
||||
|
||||
private fun assignDatabasePassword(
|
||||
databaseUri: Uri?,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
if (databaseUri != null) {
|
||||
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
|
||||
}
|
||||
}
|
||||
|
||||
fun assignPassword(mainCredential: MainCredential) {
|
||||
fun assignMainCredential(mainCredential: MainCredential) {
|
||||
mDatabase?.let { database ->
|
||||
database.fileUri?.let { databaseUri ->
|
||||
// Show the progress dialog now or after dialog confirmation
|
||||
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
|
||||
assignDatabasePassword(databaseUri, mainCredential)
|
||||
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
|
||||
} else {
|
||||
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
||||
.show(supportFragmentManager, "passwordEncodingTag")
|
||||
@@ -276,45 +182,51 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
fun saveDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
||||
mDatabaseViewModel.saveDatabase(save = true)
|
||||
}
|
||||
|
||||
fun saveDatabaseTo(uri: Uri) {
|
||||
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
|
||||
mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
|
||||
}
|
||||
|
||||
fun mergeDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
|
||||
mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
|
||||
mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
|
||||
}
|
||||
|
||||
fun reloadDatabase() {
|
||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
||||
}
|
||||
mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
|
||||
}
|
||||
|
||||
fun createEntry(newEntry: Entry,
|
||||
parent: Group) {
|
||||
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
|
||||
fun createEntry(
|
||||
newEntry: Entry,
|
||||
parent: Group
|
||||
) {
|
||||
mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun updateEntry(oldEntry: Entry,
|
||||
entryToUpdate: Entry) {
|
||||
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
||||
fun updateEntry(
|
||||
oldEntry: Entry,
|
||||
entryToUpdate: Entry
|
||||
) {
|
||||
mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun copyNodes(nodesToCopy: List<Node>,
|
||||
newParent: Group) {
|
||||
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
||||
fun copyNodes(
|
||||
nodesToCopy: List<Node>,
|
||||
newParent: Group
|
||||
) {
|
||||
mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun moveNodes(nodesToMove: List<Node>,
|
||||
newParent: Group) {
|
||||
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||
fun moveNodes(
|
||||
nodesToMove: List<Node>,
|
||||
newParent: Group
|
||||
) {
|
||||
mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
|
||||
@@ -330,6 +242,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
|
||||
// TODO Move in ViewModel
|
||||
mDatabase?.let { database ->
|
||||
// If recycle bin enabled, ensure it exists
|
||||
if (database.isRecycleBinEnabled) {
|
||||
@@ -350,11 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
||||
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
|
||||
mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun createGroup(parent: Group,
|
||||
groupInfo: GroupInfo?) {
|
||||
fun createGroup(
|
||||
parent: Group,
|
||||
groupInfo: GroupInfo?
|
||||
) {
|
||||
// TODO Move in ViewModel
|
||||
// Build the group
|
||||
mDatabase?.createGroup()?.let { newGroup ->
|
||||
groupInfo?.let { info ->
|
||||
@@ -362,12 +278,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
// Not really needed here because added in runnable but safe
|
||||
newGroup.parent = parent
|
||||
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
|
||||
mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroup(oldGroup: Group,
|
||||
groupInfo: GroupInfo) {
|
||||
fun updateGroup(
|
||||
oldGroup: Group,
|
||||
groupInfo: GroupInfo
|
||||
) {
|
||||
// TODO Move in ViewModel
|
||||
// If group updated save it in the database
|
||||
val updateGroup = Group(oldGroup).let { updateGroup ->
|
||||
updateGroup.apply {
|
||||
@@ -377,27 +296,28 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
this.setGroupInfo(groupInfo)
|
||||
}
|
||||
}
|
||||
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
||||
mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int) {
|
||||
mDatabaseTaskProvider
|
||||
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
fun restoreEntryHistory(
|
||||
mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int
|
||||
) {
|
||||
mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int) {
|
||||
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
fun deleteEntryHistory(
|
||||
mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int
|
||||
) {
|
||||
mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
private fun checkRegister() {
|
||||
// If in ave or registration mode, don't allow read only
|
||||
if ((mSpecialMode == SpecialMode.SAVE
|
||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||
&& mDatabaseReadOnly) {
|
||||
// If in registration mode, don't allow read only
|
||||
if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
|
||||
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
intent.removeModes()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -450,9 +370,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.lock) { _, _ ->
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
finish()
|
||||
}.create().show()
|
||||
} else {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,13 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
@@ -21,7 +26,7 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
|
||||
abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
|
||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||
|
||||
private var mToolbarSpecial: ToolbarSpecial? = null
|
||||
|
||||
@@ -50,8 +55,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
|
||||
fun onLaunchActivitySpecialMode() {
|
||||
if (!isIntentSender()) {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -60,8 +65,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
if (isIntentSender()) {
|
||||
super.finish()
|
||||
} else {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
backToTheMainAppAndFinish()
|
||||
}
|
||||
@@ -73,8 +78,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
// To get the app caller, only for IntentSender
|
||||
onRegularBackPressed()
|
||||
} else {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
backToTheMainAppAndFinish()
|
||||
}
|
||||
@@ -105,18 +110,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
}
|
||||
})
|
||||
|
||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||
mSpecialMode = intent.retrieveSpecialMode()
|
||||
mTypeMode = intent.retrieveTypeMode()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
||||
mSpecialMode = intent.retrieveSpecialMode()
|
||||
mTypeMode = intent.retrieveTypeMode()
|
||||
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
|
||||
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
?: intent.retrieveSearchInfo()
|
||||
|
||||
// To show the selection mode
|
||||
mToolbarSpecial = findViewById(R.id.special_mode_view)
|
||||
@@ -125,9 +130,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
val selectionModeStringId = when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT, // Not important because hidden
|
||||
SpecialMode.SEARCH -> R.string.search_mode
|
||||
SpecialMode.SAVE -> R.string.save_mode
|
||||
SpecialMode.SELECTION -> R.string.selection_mode
|
||||
SpecialMode.REGISTRATION -> R.string.registration_mode
|
||||
SpecialMode.REGISTRATION -> R.string.save_mode // Save is registration mode
|
||||
}
|
||||
val typeModeStringId = when (mTypeMode) {
|
||||
TypeMode.DEFAULT, // Not important because hidden
|
||||
@@ -145,7 +149,6 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
visible = when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> false
|
||||
SpecialMode.SEARCH -> true
|
||||
SpecialMode.SAVE -> true
|
||||
SpecialMode.SELECTION -> true
|
||||
SpecialMode.REGISTRATION -> true
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
interface DatabaseRetrieval {
|
||||
fun onDatabaseRetrieved(database: ContextualDatabase?)
|
||||
fun onDatabaseActionFinished(database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result)
|
||||
fun onDatabaseRetrieved(database: ContextualDatabase)
|
||||
|
||||
fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
)
|
||||
}
|
||||
@@ -24,26 +24,31 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
import android.widget.RemoteViews
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.getEnum
|
||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableList
|
||||
import com.kunzisoft.keepass.utils.putEnum
|
||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||
import com.kunzisoft.keepass.utils.putParcelableList
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
|
||||
object EntrySelectionHelper {
|
||||
|
||||
@@ -51,6 +56,8 @@ object EntrySelectionHelper {
|
||||
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
||||
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||
private const val EXTRA_NODES_IDS = "com.kunzisoft.keepass.extra.NODES_IDS"
|
||||
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.NODE_ID"
|
||||
|
||||
/**
|
||||
* Finish the activity by passing the result code and by locking the database if necessary
|
||||
@@ -58,7 +65,7 @@ object EntrySelectionHelper {
|
||||
fun Activity.setActivityResult(
|
||||
lockDatabase: Boolean = false,
|
||||
resultCode: Int,
|
||||
data: Intent? = null,
|
||||
data: Intent? = null
|
||||
) {
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK ->
|
||||
@@ -68,170 +75,212 @@ object EntrySelectionHelper {
|
||||
}
|
||||
this.finish()
|
||||
|
||||
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
||||
if (lockDatabase) {
|
||||
// Close the database
|
||||
this.sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to build a registerForActivityResult,
|
||||
* Used recursively, close each activity with return data
|
||||
*/
|
||||
fun AppCompatActivity.buildActivityResultLauncher(
|
||||
lockDatabase: Boolean = false,
|
||||
dataTransformation: (data: Intent?) -> Intent? = { it },
|
||||
): ActivityResultLauncher<Intent> {
|
||||
return this.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
setActivityResult(
|
||||
lockDatabase,
|
||||
it.resultCode,
|
||||
dataTransformation(it.data)
|
||||
)
|
||||
fun startActivityForSearchModeResult(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
intent.addSpecialMode(SpecialMode.SEARCH)
|
||||
intent.addSearchInfo(searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun startActivityForSelectionModeResult(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
typeMode: TypeMode,
|
||||
searchInfo: SearchInfo?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||
) {
|
||||
intent.addSpecialMode(SpecialMode.SELECTION)
|
||||
intent.addTypeMode(typeMode)
|
||||
intent.addSearchInfo(searchInfo)
|
||||
if (activityResultLauncher == null) {
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
}
|
||||
|
||||
fun startActivityForSearchModeResult(context: Context,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun startActivityForSaveModeResult(context: Context,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SAVE)
|
||||
addTypeModeInIntent(intent, TypeMode.DEFAULT)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun startActivityForKeyboardSelectionModeResult(context: Context,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo?) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to start an activity with an Autofill for result
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun startActivityForAutofillSelectionModeResult(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?
|
||||
) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
||||
intent.addAutofillComponent(context, autofillComponent)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
activityResultLauncher?.launch(intent)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
fun startActivityForPasskeySelectionModeResult(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
searchInfo: SearchInfo?
|
||||
) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
addTypeModeInIntent(intent, TypeMode.PASSKEY)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
activityResultLauncher?.launch(intent)
|
||||
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun startActivityForRegistrationModeResult(
|
||||
context: Context?,
|
||||
context: Context,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
intent: Intent,
|
||||
registerInfo: RegisterInfo?,
|
||||
typeMode: TypeMode
|
||||
) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||
addTypeModeInIntent(intent, typeMode)
|
||||
addRegisterInfoInIntent(intent, registerInfo)
|
||||
intent.addSpecialMode(SpecialMode.REGISTRATION)
|
||||
intent.addTypeMode(typeMode)
|
||||
intent.addRegisterInfo(registerInfo)
|
||||
if (activityResultLauncher == null) {
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
activityResultLauncher?.launch(intent) ?: context?.startActivity(intent) ?:
|
||||
throw IllegalStateException("At least Context or ActivityResultLauncher must not be null")
|
||||
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
||||
/**
|
||||
* Build the special mode response for internal entry selection for one entry
|
||||
*/
|
||||
fun Activity.buildSpecialModeResponseAndSetResult(
|
||||
entryInfo: EntryInfo,
|
||||
extras: Bundle? = null
|
||||
) {
|
||||
this.buildSpecialModeResponseAndSetResult(listOf(entryInfo), extras)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the special mode response for internal entry selection for multiple entries
|
||||
*/
|
||||
fun Activity.buildSpecialModeResponseAndSetResult(
|
||||
entriesInfo: List<EntryInfo>,
|
||||
extras: Bundle? = null
|
||||
) {
|
||||
try {
|
||||
val mReplyIntent = Intent()
|
||||
Log.d(javaClass.name, "Success special mode manual selection")
|
||||
mReplyIntent.addNodesIds(entriesInfo.map { it.id })
|
||||
extras?.let {
|
||||
mReplyIntent.putExtras(it)
|
||||
}
|
||||
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(javaClass.name, "Unable to add the result", e)
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
}
|
||||
|
||||
fun Intent.addSearchInfo(searchInfo: SearchInfo?): Intent {
|
||||
searchInfo?.let {
|
||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||
putExtra(KEY_SEARCH_INFO, it)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
||||
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO)
|
||||
fun Bundle.addSearchInfo(searchInfo: SearchInfo?): Bundle {
|
||||
searchInfo?.let {
|
||||
putParcelable(KEY_SEARCH_INFO, it)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
||||
return getParcelableExtraCompat(KEY_SEARCH_INFO)
|
||||
}
|
||||
|
||||
fun Bundle.getSearchInfo(): SearchInfo? {
|
||||
return getParcelableCompat(KEY_SEARCH_INFO)
|
||||
}
|
||||
|
||||
fun Intent.addRegisterInfo(registerInfo: RegisterInfo?): Intent {
|
||||
registerInfo?.let {
|
||||
intent.putExtra(KEY_REGISTER_INFO, it)
|
||||
putExtra(KEY_REGISTER_INFO, it)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
||||
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
|
||||
fun Bundle.addRegisterInfo(registerInfo: RegisterInfo?): Bundle {
|
||||
registerInfo?.let {
|
||||
putParcelable(KEY_REGISTER_INFO, it)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun removeInfoFromIntent(intent: Intent) {
|
||||
intent.removeExtra(KEY_SEARCH_INFO)
|
||||
intent.removeExtra(KEY_REGISTER_INFO)
|
||||
fun Intent.retrieveRegisterInfo(): RegisterInfo? {
|
||||
return getParcelableExtraCompat(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||
// TODO Replace by Intent.addSpecialMode
|
||||
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||
fun Bundle.getRegisterInfo(): RegisterInfo? {
|
||||
return getParcelableCompat(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun Intent.removeInfo() {
|
||||
removeExtra(KEY_SEARCH_INFO)
|
||||
removeExtra(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
|
||||
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||
return this
|
||||
}
|
||||
|
||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
||||
return SpecialMode.SELECTION
|
||||
}
|
||||
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||
fun Bundle.addSpecialMode(specialMode: SpecialMode): Bundle {
|
||||
this.putEnum(KEY_SPECIAL_MODE, specialMode)
|
||||
return this
|
||||
}
|
||||
|
||||
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||
// TODO Replace by Intent.addTypeMode
|
||||
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||
fun Intent.retrieveSpecialMode(): SpecialMode {
|
||||
return this.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||
}
|
||||
|
||||
fun Bundle.getSpecialMode(): SpecialMode {
|
||||
return this.getEnum<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||
}
|
||||
|
||||
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
|
||||
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||
return this
|
||||
}
|
||||
|
||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
||||
return TypeMode.AUTOFILL
|
||||
}
|
||||
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
|
||||
fun Intent.retrieveTypeMode(): TypeMode {
|
||||
return getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
|
||||
}
|
||||
|
||||
fun removeModesFromIntent(intent: Intent) {
|
||||
intent.removeExtra(KEY_SPECIAL_MODE)
|
||||
intent.removeExtra(KEY_TYPE_MODE)
|
||||
fun Intent.removeModes() {
|
||||
removeExtra(KEY_SPECIAL_MODE)
|
||||
removeExtra(KEY_TYPE_MODE)
|
||||
}
|
||||
|
||||
fun Intent.addNodesIds(nodesIds: List<UUID>): Intent {
|
||||
this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
|
||||
return this
|
||||
}
|
||||
|
||||
fun Intent.retrieveNodesIds(): List<UUID>? {
|
||||
return getParcelableList<ParcelUuid>(EXTRA_NODES_IDS)?.map { it.uuid }
|
||||
}
|
||||
|
||||
fun Intent.removeNodesIds() {
|
||||
removeExtra(EXTRA_NODES_IDS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the node id to the intent
|
||||
*/
|
||||
fun Intent.addNodeId(nodeId: UUID?) {
|
||||
nodeId?.let {
|
||||
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the node id from the intent
|
||||
*/
|
||||
fun Intent.retrieveNodeId(): UUID? {
|
||||
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
|
||||
}
|
||||
|
||||
fun Intent.removeNodeId() {
|
||||
removeExtra(EXTRA_NODE_ID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve nodes ids from intent and get the corresponding entry info list in [database]
|
||||
*/
|
||||
fun Intent.retrieveAndRemoveEntries(database: ContextualDatabase): List<EntryInfo> {
|
||||
val nodesIds = retrieveNodesIds()
|
||||
?: throw IOException("NodesIds is null")
|
||||
removeNodesIds()
|
||||
return nodesIds.mapNotNull { nodeId ->
|
||||
database
|
||||
.getEntryById(NodeIdUUID(nodeId))
|
||||
?.getEntryInfo(database)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,103 +288,101 @@ object EntrySelectionHelper {
|
||||
*/
|
||||
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
|
||||
return (specialMode == SpecialMode.SELECTION
|
||||
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
&& (typeMode == TypeMode.MAGIKEYBOARD || typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||
|| (specialMode == SpecialMode.REGISTRATION
|
||||
&& typeMode == TypeMode.PASSKEY)
|
||||
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||
}
|
||||
|
||||
fun doSpecialAction(intent: Intent,
|
||||
defaultAction: () -> Unit,
|
||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||
saveAction: (searchInfo: SearchInfo) -> Unit,
|
||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
||||
autofillComponent: AutofillComponent) -> Unit,
|
||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit,
|
||||
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||
|
||||
when (retrieveSpecialModeFromIntent(intent)) {
|
||||
fun doSpecialAction(
|
||||
intent: Intent,
|
||||
defaultAction: () -> Unit,
|
||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||
selectionAction: (
|
||||
intentSenderMode: Boolean,
|
||||
typeMode: TypeMode,
|
||||
searchInfo: SearchInfo?
|
||||
) -> Unit,
|
||||
registrationAction: (
|
||||
intentSenderMode: Boolean,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo?
|
||||
) -> Unit
|
||||
) {
|
||||
when (val specialMode = intent.retrieveSpecialMode()) {
|
||||
SpecialMode.DEFAULT -> {
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
defaultAction.invoke()
|
||||
}
|
||||
SpecialMode.SEARCH -> {
|
||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
val searchInfo = intent.retrieveSearchInfo()
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else {
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
SpecialMode.SAVE -> {
|
||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
if (searchInfo != null)
|
||||
saveAction.invoke(searchInfo)
|
||||
else {
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
SpecialMode.SELECTION -> {
|
||||
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
|
||||
var autofillComponentInit = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
|
||||
autofillSelectionAction.invoke(searchInfo, autofillComponent)
|
||||
autofillComponentInit = true
|
||||
}
|
||||
}
|
||||
if (!autofillComponentInit) {
|
||||
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
||||
when (retrieveTypeModeFromIntent(intent)) {
|
||||
TypeMode.DEFAULT -> {
|
||||
removeModesFromIntent(intent)
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else
|
||||
defaultAction.invoke()
|
||||
}
|
||||
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
||||
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo)
|
||||
else -> {
|
||||
// In this case, error
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
}
|
||||
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
|
||||
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
||||
when (val typeMode = intent.retrieveTypeMode()) {
|
||||
TypeMode.DEFAULT -> {
|
||||
intent.removeModes()
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else
|
||||
defaultAction.invoke()
|
||||
}
|
||||
TypeMode.MAGIKEYBOARD -> selectionAction.invoke(
|
||||
isIntentSenderMode(specialMode, typeMode),
|
||||
typeMode,
|
||||
searchInfo
|
||||
)
|
||||
TypeMode.PASSKEY ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
selectionAction.invoke(
|
||||
isIntentSenderMode(specialMode, typeMode),
|
||||
typeMode,
|
||||
searchInfo
|
||||
)
|
||||
} else
|
||||
defaultAction.invoke()
|
||||
TypeMode.AUTOFILL -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
selectionAction.invoke(
|
||||
isIntentSenderMode(specialMode, typeMode),
|
||||
typeMode,
|
||||
searchInfo
|
||||
)
|
||||
} else
|
||||
defaultAction.invoke()
|
||||
}
|
||||
} else {
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else
|
||||
defaultAction.invoke()
|
||||
}
|
||||
} else {
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
||||
if (!isIntentSenderMode(
|
||||
specialMode = retrieveSpecialModeFromIntent(intent),
|
||||
typeMode = retrieveTypeModeFromIntent(intent))
|
||||
) {
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
|
||||
val typeMode = intent.retrieveTypeMode()
|
||||
val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
|
||||
if (!intentSenderMode) {
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
}
|
||||
when (retrieveTypeModeFromIntent(intent)) {
|
||||
TypeMode.AUTOFILL -> {
|
||||
autofillRegistrationAction.invoke(registerInfo)
|
||||
}
|
||||
TypeMode.PASSKEY -> {
|
||||
passkeyRegistrationAction.invoke(registerInfo)
|
||||
}
|
||||
else -> {
|
||||
// Do other registration type
|
||||
}
|
||||
if (registerInfo != null)
|
||||
registrationAction.invoke(
|
||||
intentSenderMode,
|
||||
typeMode,
|
||||
registerInfo
|
||||
)
|
||||
else {
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,7 +414,7 @@ object EntrySelectionHelper {
|
||||
try {
|
||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||
return Icon.createWithBitmap(bitmap)
|
||||
return IconCompat.createWithBitmap(bitmap).toIcon(context)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.credentialprovider
|
||||
enum class SpecialMode {
|
||||
DEFAULT,
|
||||
SEARCH,
|
||||
SAVE,
|
||||
SELECTION,
|
||||
REGISTRATION;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.kunzisoft.keepass.credentialprovider
|
||||
|
||||
enum class TypeMode {
|
||||
DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY
|
||||
DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
|
||||
}
|
||||
@@ -27,245 +27,186 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getRegisterInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.retrieveAutofillComponent
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.utils.AppUtil
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import com.kunzisoft.keepass.view.toastError
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.buildActivityResultLauncher(lockDatabase = true)
|
||||
private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
autofillLauncherViewModel.manageSelectionResult(it)
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
autofillLauncherViewModel.manageRegistrationResult(it)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
override fun applyCustomStyle(): Boolean = false
|
||||
|
||||
// Retrieve selection mode
|
||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||
when (specialMode) {
|
||||
SpecialMode.SELECTION -> {
|
||||
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
|
||||
// To pass extra inline request
|
||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest = bundle.getParcelableCompat(
|
||||
KEY_INLINE_SUGGESTION
|
||||
)
|
||||
}
|
||||
// Build search param
|
||||
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||
AppUtil.getConcreteWebDomain(
|
||||
this,
|
||||
searchInfo.webDomain
|
||||
) { concreteWebDomain ->
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val assistStructure = AutofillHelper
|
||||
.retrieveAutofillComponent(intent)
|
||||
?.assistStructure
|
||||
val newAutofillComponent = if (assistStructure != null) {
|
||||
AutofillComponent(
|
||||
assistStructure,
|
||||
compatInlineSuggestionsRequest
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchSelection(database, newAutofillComponent, searchInfo)
|
||||
}
|
||||
}
|
||||
override fun finishActivityIfReloadRequested(): Boolean = true
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// To apply the bypass https://github.com/Kunzisoft/KeePassDX/issues/2238
|
||||
// before managing intent in super class
|
||||
intent.retrieveSelectionBundle()?.apply {
|
||||
intent.addSpecialMode(getSpecialMode())
|
||||
intent.addSearchInfo(getSearchInfo())
|
||||
intent.addRegisterInfo(getRegisterInfo())
|
||||
intent.addAutofillComponent(retrieveAutofillComponent())
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
autofillLauncherViewModel.initialize()
|
||||
lifecycleScope.launch {
|
||||
// Initialize the parameters
|
||||
autofillLauncherViewModel.uiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
AutofillLauncherViewModel.UIState.Loading -> {}
|
||||
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
|
||||
showBlockRestartMessage()
|
||||
autofillLauncherViewModel.cancelResult()
|
||||
}
|
||||
// Remove bundle
|
||||
intent.removeExtra(KEY_SELECTION_BUNDLE)
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
// To register info
|
||||
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(
|
||||
KEY_REGISTER_INFO
|
||||
)
|
||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchRegistration(database, searchInfo, registerInfo)
|
||||
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
|
||||
showAutofillSuggestionMessage()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Not an autofill call
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
// Retrieve the UI
|
||||
autofillLauncherViewModel.credentialUiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is CredentialLauncherViewModel.CredentialState.Loading -> {}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
|
||||
GroupActivity.launchForSelection(
|
||||
context = this@AutofillLauncherActivity,
|
||||
database = uiState.database,
|
||||
searchInfo = uiState.searchInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mAutofillSelectionActivityResultLauncher,
|
||||
)
|
||||
}
|
||||
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?,
|
||||
autofillComponent: AutofillComponent?,
|
||||
searchInfo: SearchInfo) {
|
||||
if (autofillComponent == null) {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
} else if (KeeAutofillService.autofillAllowedFor(
|
||||
applicationId = searchInfo.applicationId,
|
||||
webDomain = searchInfo.webDomain,
|
||||
context = this
|
||||
)) {
|
||||
// If database is open
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
// Items found
|
||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||
finish()
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillSelectionResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
mCredentialActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false
|
||||
)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(
|
||||
this,
|
||||
mCredentialActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showBlockRestartMessage()
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchRegistration(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
registerInfo: RegisterInfo?) {
|
||||
if (KeeAutofillService.autofillAllowedFor(
|
||||
applicationId = searchInfo.applicationId,
|
||||
webDomain = searchInfo.webDomain,
|
||||
context = this
|
||||
)) {
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, _ ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(
|
||||
context = this,
|
||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(
|
||||
context = this,
|
||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForRegistration(
|
||||
context = this,
|
||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showBlockRestartMessage()
|
||||
setResult(RESULT_CANCELED)
|
||||
}
|
||||
finish()
|
||||
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onUnknownDatabaseRetrieved(database)
|
||||
autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||
}
|
||||
|
||||
private fun showBlockRestartMessage() {
|
||||
// If item not allowed, show a toast
|
||||
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.autofill_block_restart,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun showReadOnlySaveMessage() {
|
||||
toastError(RegisterInReadOnlyDatabaseException())
|
||||
private fun showAutofillSuggestionMessage() {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.autofill_inline_suggestions_keyboard,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_PENDING_INTENT_BUNDLE = "com.kunzisoft.keepass.extra.BUNDLE"
|
||||
private val TAG = AutofillLauncherActivity::class.java.name
|
||||
|
||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
||||
fun Intent.retrieveSelectionBundle(): Bundle? {
|
||||
return this.getBundleExtra(KEY_PENDING_INTENT_BUNDLE)
|
||||
}
|
||||
|
||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||
|
||||
fun getPendingIntentForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? {
|
||||
fun getPendingIntentForSelection(
|
||||
context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
autofillComponent: AutofillComponent
|
||||
): PendingIntent? {
|
||||
try {
|
||||
// Doesn't work with direct extra Parcelable in Android 11 (don't know why?)
|
||||
// https://github.com/Kunzisoft/KeePassDX/issues/2238
|
||||
// Wrap into a bundle to bypass the problem
|
||||
val tempBundle = Bundle().apply {
|
||||
addSpecialMode(SpecialMode.SELECTION)
|
||||
addSearchInfo(searchInfo)
|
||||
addAutofillComponent(autofillComponent)
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context, 0,
|
||||
// Doesn't work with direct extra Parcelable (don't know why?)
|
||||
// Wrap into a bundle to bypass the problem
|
||||
context,
|
||||
randomRequestCode(),
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
||||
}
|
||||
})
|
||||
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
@@ -279,14 +220,21 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun getPendingIntentForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): PendingIntent? {
|
||||
fun getPendingIntentForRegistration(
|
||||
context: Context,
|
||||
registerInfo: RegisterInfo
|
||||
): PendingIntent? {
|
||||
try {
|
||||
// Bypass intent issue
|
||||
val tempBundle = Bundle().apply {
|
||||
addSpecialMode(SpecialMode.REGISTRATION)
|
||||
addRegisterInfo(registerInfo)
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context, 0,
|
||||
context,
|
||||
randomRequestCode(),
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
@@ -299,14 +247,5 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun launchForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo) {
|
||||
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,22 +22,22 @@ package com.kunzisoft.keepass.credentialprovider.activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import com.kunzisoft.keepass.R
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.EntrySelectionViewModel
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.utils.AppUtil
|
||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.view.toastError
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Activity to search or select entry in database,
|
||||
@@ -45,198 +45,133 @@ import com.kunzisoft.keepass.view.toastError
|
||||
*/
|
||||
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
private val entrySelectionViewModel: EntrySelectionViewModel by viewModels()
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
entrySelectionViewModel.manageSelectionResult(it)
|
||||
}
|
||||
|
||||
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launch(database, searchInfo)
|
||||
}
|
||||
}
|
||||
override fun applyCustomStyle() = false
|
||||
|
||||
private fun launch(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo) {
|
||||
override fun finishActivityIfReloadRequested() = false
|
||||
|
||||
// Setting to integrate Magikeyboard
|
||||
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
// If database is open
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
// Items found
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
entrySelectionViewModel.initialize()
|
||||
lifecycleScope.launch {
|
||||
// Initialize the parameters
|
||||
entrySelectionViewModel.uiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is EntrySelectionViewModel.UIState.Loading -> {}
|
||||
is EntrySelectionViewModel.UIState.PopulateKeyboard -> {
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
entry = uiState.entryInfo,
|
||||
toast = true
|
||||
)
|
||||
} else {
|
||||
toastError(RegisterInReadOnlyDatabaseException())
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
// Automatically populate keyboard
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entryInfo
|
||||
)
|
||||
},
|
||||
{ autoSearch ->
|
||||
GroupActivity.launchForKeyboardSelectionResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
autoSearch
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true
|
||||
)
|
||||
}
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false
|
||||
is EntrySelectionViewModel.UIState.LaunchFileDatabaseSelectForSearch -> {
|
||||
FileDatabaseSelectActivity.launchForSearch(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
searchInfo = uiState.searchInfo
|
||||
)
|
||||
} else {
|
||||
toastError(RegisterInReadOnlyDatabaseException())
|
||||
finish()
|
||||
}
|
||||
is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> {
|
||||
GroupActivity.launchForSearch(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
database = uiState.database,
|
||||
searchInfo = uiState.searchInfo
|
||||
)
|
||||
finish()
|
||||
}
|
||||
} 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 {
|
||||
|
||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||
|
||||
fun launch(context: Context,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
|
||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||
})
|
||||
}
|
||||
// New task needed because don't launch from an Activity context
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
fun launch(
|
||||
context: Context,
|
||||
searchInfo: SearchInfo? = null
|
||||
) {
|
||||
context.startActivity(Intent(
|
||||
context,
|
||||
EntrySelectionLauncherActivity::class.java
|
||||
).apply {
|
||||
addSearchInfo(searchInfo)
|
||||
// New task needed because don't launch from an Activity context
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.GroupActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||
@@ -44,14 +46,14 @@ import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.model.AppOrigin
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import com.kunzisoft.keepass.view.toastError
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
@@ -79,10 +81,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
@@ -105,61 +103,69 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||
nodeId = uiState.nodeId
|
||||
)
|
||||
}
|
||||
is PasskeyLauncherViewModel.UIState.SetActivityResult -> {
|
||||
setActivityResult(
|
||||
lockDatabase = uiState.lockDatabase,
|
||||
resultCode = uiState.resultCode,
|
||||
data = uiState.data
|
||||
)
|
||||
}
|
||||
is PasskeyLauncherViewModel.UIState.ShowError -> {
|
||||
toastError(uiState.error)
|
||||
passkeyLauncherViewModel.cancelResult()
|
||||
}
|
||||
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
|
||||
GroupActivity.launchForPasskeySelectionResult(
|
||||
context = this@PasskeyLauncherActivity,
|
||||
database = uiState.database,
|
||||
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
||||
searchInfo = null,
|
||||
autoSearch = false
|
||||
)
|
||||
}
|
||||
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
|
||||
GroupActivity.launchForRegistration(
|
||||
context = this@PasskeyLauncherActivity,
|
||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||
database = uiState.database,
|
||||
registerInfo = uiState.registerInfo,
|
||||
typeMode = uiState.typeMode
|
||||
)
|
||||
}
|
||||
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
|
||||
activity = this@PasskeyLauncherActivity,
|
||||
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
||||
searchInfo = uiState.searchInfo,
|
||||
)
|
||||
}
|
||||
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||
FileDatabaseSelectActivity.launchForRegistration(
|
||||
context = this@PasskeyLauncherActivity,
|
||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||
registerInfo = uiState.registerInfo,
|
||||
typeMode = uiState.typeMode
|
||||
)
|
||||
}
|
||||
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
|
||||
updateEntry(uiState.oldEntry, uiState.newEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
passkeyLauncherViewModel.credentialUiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is CredentialLauncherViewModel.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?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database)
|
||||
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onUnknownDatabaseRetrieved(database)
|
||||
passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -170,7 +176,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||
passkeyLauncherViewModel.autoSelectPasskey(result, database)
|
||||
// TODO When auto save is enabled, WARNING filter by the calling activity
|
||||
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,6 +241,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||
)
|
||||
.append("\n\n")
|
||||
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
|
||||
.append("\n\n")
|
||||
.append(getString(R.string.passkeys_missing_signature_app_ask_question))
|
||||
.toString()
|
||||
)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
@@ -273,7 +282,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||
): PendingIntent? {
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
(Math.random() * Integer.MAX_VALUE).toInt(),
|
||||
randomRequestCode(),
|
||||
Intent(context, PasskeyLauncherActivity::class.java).apply {
|
||||
addSpecialMode(specialMode)
|
||||
addTypeMode(TypeMode.PASSKEY)
|
||||
|
||||
@@ -2,5 +2,7 @@ package com.kunzisoft.keepass.credentialprovider.autofill
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
|
||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
||||
data class AutofillComponent(
|
||||
val assistStructure: AssistStructure,
|
||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?
|
||||
)
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
@@ -28,6 +27,7 @@ import android.content.Intent
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.Field
|
||||
import android.service.autofill.FillResponse
|
||||
@@ -38,7 +38,6 @@ import android.view.autofill.AutofillId
|
||||
import android.view.autofill.AutofillManager
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.autofill.inline.UiVersions
|
||||
@@ -54,21 +53,63 @@ import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import java.io.IOException
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
object AutofillHelper {
|
||||
|
||||
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||
private const val EXTRA_BASE_STRUCTURE = "com.kunzisoft.keepass.autofill.BASE_STRUCTURE"
|
||||
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||
|
||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||
fun Intent.addAutofillComponent(autofillComponent: AutofillComponent?): Intent {
|
||||
autofillComponent?.let {
|
||||
this.putExtra(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Intent.retrieveAutofillComponent(): AutofillComponent? {
|
||||
this.getParcelableExtraCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
AutofillComponent(assistStructure,
|
||||
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||
AutofillComponent(
|
||||
assistStructure,
|
||||
this.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||
} else {
|
||||
AutofillComponent(assistStructure, null)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun Bundle.addAutofillComponent(autofillComponent: AutofillComponent?): Bundle {
|
||||
autofillComponent?.let {
|
||||
this.putParcelable(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||
this.putParcelable(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Bundle.retrieveAutofillComponent(): AutofillComponent? {
|
||||
this.getParcelableCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
AutofillComponent(
|
||||
assistStructure,
|
||||
this.getParcelableCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
)
|
||||
} else {
|
||||
AutofillComponent(assistStructure, null)
|
||||
}
|
||||
@@ -127,11 +168,13 @@ object AutofillHelper {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun buildDatasetForEntry(context: Context,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?): Dataset {
|
||||
private fun buildDatasetForEntry(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?
|
||||
): Dataset {
|
||||
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
|
||||
|
||||
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
@@ -291,11 +334,13 @@ object AutofillHelper {
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun buildInlinePresentationForEntry(context: Context,
|
||||
database: ContextualDatabase,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo): InlinePresentation? {
|
||||
private fun buildInlinePresentationForEntry(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo
|
||||
): InlinePresentation? {
|
||||
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||
@@ -314,7 +359,7 @@ object AutofillHelper {
|
||||
// Build the content for IME UI
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
randomRequestCode(),
|
||||
Intent(context, AutofillSettingsActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
@@ -341,9 +386,11 @@ object AutofillHelper {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForManualSelection(context: Context,
|
||||
inlinePresentationSpec: InlinePresentationSpec,
|
||||
pendingIntent: PendingIntent): InlinePresentation? {
|
||||
private fun buildInlinePresentationForManualSelection(
|
||||
context: Context,
|
||||
inlinePresentationSpec: InlinePresentationSpec,
|
||||
pendingIntent: PendingIntent
|
||||
): InlinePresentation? {
|
||||
// Make sure that the IME spec claims support for v1 UI template.
|
||||
val imeStyle = inlinePresentationSpec.style
|
||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||
@@ -360,11 +407,13 @@ object AutofillHelper {
|
||||
}.build().slice, inlinePresentationSpec, false)
|
||||
}
|
||||
|
||||
fun buildResponse(context: Context,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
||||
fun buildResponse(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
autofillComponent: AutofillComponent
|
||||
): FillResponse? {
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
// Add Header
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -385,7 +434,8 @@ object AutofillHelper {
|
||||
// Add inline suggestion for new IME and dataset
|
||||
var numberInlineSuggestions = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
autofillComponent.compatInlineSuggestionsRequest
|
||||
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||
@@ -401,21 +451,27 @@ object AutofillHelper {
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& numberInlineSuggestions > 0
|
||||
&& compatInlineSuggestionsRequest != null) {
|
||||
&& autofillComponent.compatInlineSuggestionsRequest != null) {
|
||||
inlinePresentation = buildInlinePresentationForEntry(
|
||||
context,
|
||||
database,
|
||||
compatInlineSuggestionsRequest,
|
||||
autofillComponent.compatInlineSuggestionsRequest,
|
||||
numberInlineSuggestions--,
|
||||
entry
|
||||
)
|
||||
}
|
||||
// Create dataset for each entry
|
||||
responseBuilder.addDataset(
|
||||
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
|
||||
buildDatasetForEntry(
|
||||
context = context,
|
||||
database = database,
|
||||
entryInfo = entry,
|
||||
struct = parseResult,
|
||||
inlinePresentation = inlinePresentation
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to add dataset")
|
||||
Log.e(TAG, "Unable to add dataset", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,21 +483,28 @@ object AutofillHelper {
|
||||
webScheme = parseResult.webScheme
|
||||
manualSelection = true
|
||||
}
|
||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||
AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||
searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent ->
|
||||
val manualSelectionView = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.item_autofill_select_entry
|
||||
)
|
||||
AutofillLauncherActivity.getPendingIntentForSelection(
|
||||
context,
|
||||
searchInfo,
|
||||
autofillComponent
|
||||
)?.let { pendingIntent ->
|
||||
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpec =
|
||||
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
inlinePresentation = buildInlinePresentationForManualSelection(
|
||||
context,
|
||||
inlinePresentationSpec,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
autofillComponent.compatInlineSuggestionsRequest
|
||||
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpec =
|
||||
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
inlinePresentation = buildInlinePresentationForManualSelection(
|
||||
context,
|
||||
inlinePresentationSpec,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
@@ -486,61 +549,31 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Autofill response for one entry
|
||||
* Build the Autofill response
|
||||
*/
|
||||
fun buildResponseAndSetResult(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo) {
|
||||
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Autofill response for many entry
|
||||
*/
|
||||
fun buildResponseAndSetResult(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>) {
|
||||
fun buildResponse(
|
||||
context: Context,
|
||||
autofillComponent: AutofillComponent,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
onIntentCreated: (Intent) -> Unit
|
||||
) {
|
||||
if (entriesInfo.isEmpty()) {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
throw IOException("No entries found")
|
||||
} else {
|
||||
var setResultOk = false
|
||||
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(
|
||||
EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||
)
|
||||
if (compatInlineSuggestionsRequest != null) {
|
||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
|
||||
} else {
|
||||
buildResponse(activity, database, entriesInfo, result, null)
|
||||
}
|
||||
val mReplyIntent = Intent()
|
||||
Log.d(activity.javaClass.name, "Success Autofill auth.")
|
||||
mReplyIntent.putExtra(
|
||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||
response)
|
||||
setResultOk = true
|
||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
||||
}
|
||||
}
|
||||
if (!setResultOk) {
|
||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
|
||||
this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
|
||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
|
||||
// New Response
|
||||
onIntentCreated(Intent().putExtra(
|
||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||
buildResponse(
|
||||
context = context,
|
||||
database = database,
|
||||
entriesInfo = entriesInfo,
|
||||
parseResult = result,
|
||||
autofillComponent = autofillComponent
|
||||
)
|
||||
))
|
||||
} ?: throw IOException("Unable to parse the structure")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.AppUtil
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import org.joda.time.DateTime
|
||||
|
||||
|
||||
@@ -92,10 +92,11 @@ class KeeAutofillService : AutofillService() {
|
||||
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
|
||||
}
|
||||
|
||||
override fun onFillRequest(request: FillRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: FillCallback) {
|
||||
|
||||
override fun onFillRequest(
|
||||
request: FillRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: FillCallback
|
||||
) {
|
||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||
|
||||
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
|
||||
@@ -120,67 +121,64 @@ class KeeAutofillService : AutofillService() {
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
}
|
||||
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
CompatInlineSuggestionsRequest(request)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
launchSelection(mDatabase,
|
||||
searchInfo,
|
||||
parseResult,
|
||||
inlineSuggestionsRequest,
|
||||
callback)
|
||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
CompatInlineSuggestionsRequest(request)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val autofillComponent = AutofillComponent(
|
||||
latestStructure,
|
||||
inlineSuggestionsRequest
|
||||
)
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = mDatabase,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
callback.onSuccess(
|
||||
AutofillHelper.buildResponse(
|
||||
context = this,
|
||||
database = openedDatabase,
|
||||
entriesInfo = items,
|
||||
parseResult = parseResult,
|
||||
autofillComponent = autofillComponent
|
||||
)
|
||||
)
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult, openedDatabase,
|
||||
searchInfo, autofillComponent, callback)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult, null,
|
||||
searchInfo, autofillComponent, callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSelection(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
callback.onSuccess(
|
||||
AutofillHelper.buildResponse(
|
||||
this, openedDatabase,
|
||||
items, parseResult, inlineSuggestionsRequest
|
||||
)
|
||||
)
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult, openedDatabase,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult, null,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
private fun showUIForEntrySelection(
|
||||
parseResult: StructureParser.Result,
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
autofillComponent: AutofillComponent,
|
||||
callback: FillCallback
|
||||
) {
|
||||
var success = false
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||
// to generate Response.
|
||||
AutofillLauncherActivity.getPendingIntentForSelection(this,
|
||||
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender ->
|
||||
AutofillLauncherActivity.getPendingIntentForSelection(
|
||||
this,
|
||||
searchInfo,
|
||||
autofillComponent
|
||||
)?.intentSender?.let { intentSender ->
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
@@ -271,7 +269,8 @@ class KeeAutofillService : AutofillService() {
|
||||
&& autofillInlineSuggestionsEnabled
|
||||
) {
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
autofillComponent.compatInlineSuggestionsRequest
|
||||
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs =
|
||||
inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||
@@ -289,7 +288,7 @@ class KeeAutofillService : AutofillService() {
|
||||
InlineSuggestionUi.newContentBuilder(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
randomRequestCode(),
|
||||
Intent(this, AutofillSettingsActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
@@ -361,7 +360,7 @@ class KeeAutofillService : AutofillService() {
|
||||
|
||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||
var success = false
|
||||
if (askToSaveData) {
|
||||
if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val latestStructure = request.fillContexts.last().structure
|
||||
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||
|
||||
@@ -387,32 +386,32 @@ class KeeAutofillService : AutofillService() {
|
||||
}
|
||||
|
||||
// Show UI to save data
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
}
|
||||
val registerInfo = RegisterInfo(
|
||||
searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
},
|
||||
searchInfo = searchInfo,
|
||||
username = parseResult.usernameValue?.textValue?.toString(),
|
||||
password = parseResult.passwordValue?.textValue?.toString(),
|
||||
creditCard =
|
||||
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
|
||||
CreditCard(
|
||||
parseResult.creditCardHolder,
|
||||
parseResult.creditCardNumber,
|
||||
cardNumber,
|
||||
expiration,
|
||||
parseResult.cardVerificationValue
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// TODO Callback in each activity #765
|
||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
||||
// registerInfo))
|
||||
//} else {
|
||||
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
||||
success = true
|
||||
callback.onSuccess()
|
||||
//}
|
||||
AutofillLauncherActivity.getPendingIntentForRegistration(
|
||||
this,
|
||||
registerInfo
|
||||
)?.intentSender?.let { intentSender ->
|
||||
success = true
|
||||
callback.onSuccess(intentSender)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,8 +362,8 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
if (result?.passwordId == null) {
|
||||
usernameIdCandidate = autofillId
|
||||
usernameValueCandidate = node.autofillValue
|
||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||
}
|
||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||
}
|
||||
inputIsVariationType(inputType,
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||
|
||||
@@ -43,6 +43,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
@@ -461,9 +462,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
||||
}
|
||||
|
||||
fun performSelection(items: List<EntryInfo>,
|
||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||
fun performSelection(
|
||||
items: List<EntryInfo>,
|
||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||
actionEntrySelection: (autoSearch: Boolean) -> Unit
|
||||
) {
|
||||
EntrySelectionHelper.performSelection(
|
||||
items = items,
|
||||
actionPopulateCredentialProvider = { itemFound ->
|
||||
@@ -477,15 +480,5 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
actionEntrySelection = actionEntrySelection
|
||||
)
|
||||
}
|
||||
|
||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||
entry: EntryInfo,
|
||||
toast: Boolean = true) {
|
||||
// Populate Magikeyboard with entry
|
||||
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||
// Consume the selection mode
|
||||
EntrySelectionHelper.removeModesFromIntent(activity.intent)
|
||||
activity.moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,22 +93,18 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
|
||||
return SearchInfo().apply {
|
||||
this.relyingParty = relyingParty
|
||||
this.isAPasskeySearch = true
|
||||
this.query = relyingParty
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBeginGetCredentialRequest(
|
||||
request: BeginGetCredentialRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
||||
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
|
||||
) {
|
||||
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
|
||||
try {
|
||||
processGetCredentialsRequest(request)?.let { response ->
|
||||
processGetCredentialsRequest(request) { response ->
|
||||
callback.onResult(response)
|
||||
} ?: run {
|
||||
callback.onError(GetCredentialUnknownException())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
|
||||
@@ -116,24 +112,30 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? {
|
||||
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||
|
||||
private fun processGetCredentialsRequest(
|
||||
request: BeginGetCredentialRequest,
|
||||
callback: (BeginGetCredentialResponse?) -> Unit
|
||||
) {
|
||||
var knownOption = false
|
||||
for (option in request.beginGetCredentialOptions) {
|
||||
when (option) {
|
||||
is BeginGetPublicKeyCredentialOption -> {
|
||||
credentialEntries.addAll(
|
||||
populatePasskeyData(option)
|
||||
)
|
||||
return BeginGetCredentialResponse(credentialEntries)
|
||||
knownOption = true
|
||||
populatePasskeyData(option) { listCredentials ->
|
||||
callback(BeginGetCredentialResponse(listCredentials))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption")
|
||||
return null
|
||||
if (knownOption.not()) {
|
||||
throw IOException("unknown type of beginGetCredentialOption")
|
||||
}
|
||||
}
|
||||
|
||||
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
|
||||
private fun populatePasskeyData(
|
||||
option: BeginGetPublicKeyCredentialOption,
|
||||
callback: (List<CredentialEntry>) -> Unit
|
||||
) {
|
||||
|
||||
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||
|
||||
@@ -169,6 +171,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
)
|
||||
}
|
||||
}
|
||||
callback(passkeyEntries)
|
||||
},
|
||||
onItemNotFound = { _ ->
|
||||
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
||||
@@ -191,6 +194,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
)
|
||||
)
|
||||
}
|
||||
callback(passkeyEntries)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
||||
@@ -213,9 +217,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
)
|
||||
)
|
||||
}
|
||||
callback(passkeyEntries)
|
||||
}
|
||||
)
|
||||
return passkeyEntries
|
||||
}
|
||||
|
||||
override fun onBeginCreateCredentialRequest(
|
||||
@@ -225,7 +229,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
) {
|
||||
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
|
||||
try {
|
||||
callback.onResult(processCreateCredentialRequest(request))
|
||||
processCreateCredentialRequest(request) {
|
||||
callback.onResult(BeginCreateCredentialResponse(it))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
|
||||
toastError(e)
|
||||
@@ -233,15 +239,20 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse {
|
||||
private fun processCreateCredentialRequest(
|
||||
request: BeginCreateCredentialRequest,
|
||||
callback: (List<CreateEntry>) -> Unit
|
||||
) {
|
||||
when (request) {
|
||||
is BeginCreatePublicKeyCredentialRequest -> {
|
||||
// Request is passkey type
|
||||
return handleCreatePasskeyQuery(request)
|
||||
handleCreatePasskeyQuery(request, callback)
|
||||
}
|
||||
else -> {
|
||||
// request type not supported
|
||||
throw IOException("unknown type of BeginCreateCredentialRequest")
|
||||
}
|
||||
}
|
||||
// request type not supported
|
||||
throw IOException("unknown type of BeginCreateCredentialRequest")
|
||||
}
|
||||
|
||||
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
||||
@@ -266,9 +277,15 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse {
|
||||
|
||||
val accountName = mDatabase?.name ?: getString(R.string.passkey_database_username)
|
||||
private fun handleCreatePasskeyQuery(
|
||||
request: BeginCreatePublicKeyCredentialRequest,
|
||||
callback: (List<CreateEntry>) -> Unit
|
||||
) {
|
||||
val databaseName = mDatabase?.name
|
||||
val accountName =
|
||||
if (databaseName?.isBlank() != false)
|
||||
getString(R.string.passkey_database_username)
|
||||
else databaseName
|
||||
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
||||
val relyingPartyId = PublicKeyCredentialCreationOptions(
|
||||
requestJson = request.requestJson,
|
||||
@@ -309,6 +326,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
}
|
||||
}*/
|
||||
}
|
||||
callback(createEntries)
|
||||
},
|
||||
onItemNotFound = { database ->
|
||||
// To create a new entry
|
||||
@@ -317,6 +335,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
} else {
|
||||
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||
}
|
||||
callback(createEntries)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// Launch the passkey launcher activity to open the database
|
||||
@@ -334,10 +353,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
)
|
||||
)
|
||||
}
|
||||
callback(createEntries)
|
||||
}
|
||||
)
|
||||
|
||||
return BeginCreateCredentialResponse(createEntries)
|
||||
}
|
||||
|
||||
override fun onClearCredentialStateRequest(
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelUuid
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Log
|
||||
@@ -44,6 +43,7 @@ import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||
import com.kunzisoft.encrypt.Signature
|
||||
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
|
||||
@@ -60,7 +60,6 @@ import com.kunzisoft.keepass.model.AndroidOrigin
|
||||
import com.kunzisoft.keepass.model.AppOrigin
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Passkey
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.utils.AppUtil
|
||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
@@ -88,10 +87,7 @@ object PasskeyHelper {
|
||||
|
||||
private const val HMAC_TYPE = "HmacSHA256"
|
||||
|
||||
|
||||
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
|
||||
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
||||
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
|
||||
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
|
||||
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
|
||||
|
||||
@@ -110,38 +106,6 @@ object PasskeyHelper {
|
||||
|
||||
private val internalSecureRandom: SecureRandom = SecureRandom()
|
||||
|
||||
/**
|
||||
* Build the Passkey response for one entry
|
||||
*/
|
||||
fun Activity.buildPasskeyResponseAndSetResult(
|
||||
entryInfo: EntryInfo,
|
||||
extras: Bundle? = null
|
||||
) {
|
||||
try {
|
||||
entryInfo.passkey?.let { passkey ->
|
||||
val mReplyIntent = Intent()
|
||||
Log.d(javaClass.name, "Success Passkey manual selection")
|
||||
mReplyIntent.addPasskey(passkey)
|
||||
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
|
||||
mReplyIntent.addNodeId(entryInfo.id)
|
||||
extras?.let {
|
||||
mReplyIntent.putExtras(it)
|
||||
}
|
||||
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||
} ?: run {
|
||||
throw IOException("No passkey found")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(javaClass.name, "Unable to add the passkey as result", e)
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.error_passkey_result),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an authentication code generated by an entry to the intent
|
||||
*/
|
||||
@@ -181,22 +145,6 @@ object PasskeyHelper {
|
||||
return this.removeExtra(EXTRA_PASSKEY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the search info to the intent
|
||||
*/
|
||||
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
|
||||
searchInfo?.let {
|
||||
putExtra(EXTRA_SEARCH_INFO, searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the search info from the intent
|
||||
*/
|
||||
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
||||
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the app origin to the intent
|
||||
*/
|
||||
@@ -221,21 +169,37 @@ object PasskeyHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the node id to the intent, useful for auto passkey selection
|
||||
* Build the Passkey response for one entry
|
||||
*/
|
||||
fun Intent.addNodeId(nodeId: UUID?) {
|
||||
nodeId?.let {
|
||||
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
|
||||
fun Activity.buildPasskeyResponseAndSetResult(
|
||||
entryInfo: EntryInfo,
|
||||
extras: Bundle? = null
|
||||
) {
|
||||
try {
|
||||
entryInfo.passkey?.let { passkey ->
|
||||
val mReplyIntent = Intent()
|
||||
Log.d(javaClass.name, "Success Passkey manual selection")
|
||||
mReplyIntent.addPasskey(passkey)
|
||||
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
|
||||
mReplyIntent.addNodeId(entryInfo.id)
|
||||
extras?.let {
|
||||
mReplyIntent.putExtras(it)
|
||||
}
|
||||
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||
} ?: run {
|
||||
throw IOException("No passkey found")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(javaClass.name, "Unable to add the passkey as result", e)
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.error_passkey_result),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the node id from the intent
|
||||
*/
|
||||
fun Intent.retrieveNodeId(): UUID? {
|
||||
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the timestamp and authentication code transmitted via PendingIntent
|
||||
*/
|
||||
@@ -424,11 +388,15 @@ object PasskeyHelper {
|
||||
* Utility method to create a passkey and the associated creation request parameters
|
||||
* [intent] allows to retrieve the request
|
||||
* [context] context to manage package verification files
|
||||
* [defaultBackupEligibility] the default backup eligibility to add the the passkey entry
|
||||
* [defaultBackupState] the default backup state to add the the passkey entry
|
||||
* [passkeyCreated] is called asynchronously when the passkey has been created
|
||||
*/
|
||||
suspend fun retrievePasskeyCreationRequestParameters(
|
||||
intent: Intent,
|
||||
context: Context,
|
||||
defaultBackupEligibility: Boolean?,
|
||||
defaultBackupState: Boolean?,
|
||||
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
|
||||
) {
|
||||
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||
@@ -456,7 +424,9 @@ object PasskeyHelper {
|
||||
privateKeyPem = privateKeyPem,
|
||||
credentialId = b64Encode(credentialId),
|
||||
userHandle = b64Encode(userHandle),
|
||||
relyingParty = relyingParty
|
||||
relyingParty = relyingParty,
|
||||
backupEligibility = defaultBackupEligibility,
|
||||
backupState = defaultBackupState
|
||||
)
|
||||
|
||||
// create new entry in database
|
||||
@@ -590,8 +560,8 @@ object PasskeyHelper {
|
||||
requestOptions: PublicKeyCredentialRequestOptions,
|
||||
clientDataResponse: ClientDataResponse,
|
||||
passkey: Passkey,
|
||||
backupEligibility: Boolean,
|
||||
backupState: Boolean
|
||||
defaultBackupEligibility: Boolean,
|
||||
defaultBackupState: Boolean
|
||||
): PublicKeyCredential {
|
||||
val getCredentialResponse = FidoPublicKeyCredential(
|
||||
id = passkey.credentialId,
|
||||
@@ -599,8 +569,8 @@ object PasskeyHelper {
|
||||
requestOptions = requestOptions,
|
||||
userPresent = true,
|
||||
userVerified = true,
|
||||
backupEligibility = backupEligibility,
|
||||
backupState = backupState,
|
||||
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
|
||||
backupState = passkey.backupState ?: defaultBackupState,
|
||||
userHandle = passkey.userHandle,
|
||||
privateKey = passkey.privateKeyPem,
|
||||
clientDataResponse = clientDataResponse
|
||||
|
||||
@@ -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.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeNodeId
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||
@@ -25,11 +28,9 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVe
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
@@ -56,22 +57,21 @@ import java.io.InvalidObjectException
|
||||
import java.util.UUID
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
class PasskeyLauncherViewModel(application: Application): AndroidViewModel(application) {
|
||||
class PasskeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
|
||||
|
||||
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
|
||||
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
|
||||
private var mPasskey: Passkey? = null
|
||||
|
||||
private var mLockDatabaseAfterSelection: Boolean = false
|
||||
private var mBackupEligibility: Boolean = true
|
||||
private var mBackupState: Boolean = false
|
||||
private var mLockDatabase: Boolean = true
|
||||
|
||||
private var isResultLauncherRegistered: Boolean = false
|
||||
|
||||
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||
val uiState: StateFlow<UIState> = _uiState
|
||||
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||
val uiState: StateFlow<UIState> = mUiState
|
||||
|
||||
fun initialize() {
|
||||
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
|
||||
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
|
||||
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
|
||||
}
|
||||
@@ -79,19 +79,14 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
fun showAppPrivilegedDialog(
|
||||
temptingApp: AndroidPrivilegedApp
|
||||
) {
|
||||
_uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
|
||||
mUiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
|
||||
}
|
||||
|
||||
fun showAppSignatureDialog(
|
||||
temptingApp: AppOrigin,
|
||||
nodeId: UUID
|
||||
) {
|
||||
_uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
|
||||
}
|
||||
|
||||
fun showError(error: Throwable) {
|
||||
Log.e(TAG, "Error on passkey launch", error)
|
||||
_uiState.value = UIState.ShowError(error)
|
||||
mUiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
|
||||
}
|
||||
|
||||
fun saveCustomPrivilegedApp(
|
||||
@@ -107,7 +102,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
context = getApplication(),
|
||||
privilegedApps = listOf(temptingApp)
|
||||
)
|
||||
launchPasskeyAction(
|
||||
launchAction(
|
||||
intent = intent,
|
||||
specialMode = specialMode,
|
||||
database = database
|
||||
@@ -139,54 +134,33 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
)
|
||||
entryInfo.saveAppOrigin(database, temptingApp)
|
||||
newEntry.setEntryInfo(database, entryInfo)
|
||||
_uiState.value = UIState.UpdateEntry(
|
||||
mUiState.value = UIState.UpdateEntry(
|
||||
oldEntry = entry,
|
||||
newEntry = newEntry
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setResult(intent: Intent) {
|
||||
// Remove the launcher register
|
||||
isResultLauncherRegistered = false
|
||||
_uiState.value = UIState.SetActivityResult(
|
||||
lockDatabase = mLockDatabase,
|
||||
resultCode = RESULT_OK,
|
||||
data = intent
|
||||
)
|
||||
override fun onExceptionOccurred(e: Throwable) {
|
||||
if (e is PrivilegedAllowLists.PrivilegedException) {
|
||||
showAppPrivilegedDialog(e.temptingApp)
|
||||
} else {
|
||||
super.onExceptionOccurred(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelResult() {
|
||||
isResultLauncherRegistered = false
|
||||
_uiState.value = UIState.SetActivityResult(
|
||||
lockDatabase = mLockDatabase,
|
||||
resultCode = RESULT_CANCELED
|
||||
)
|
||||
}
|
||||
|
||||
fun launchPasskeyActionIfNeeded(
|
||||
override fun launchActionIfNeeded(
|
||||
intent: Intent,
|
||||
specialMode: SpecialMode,
|
||||
database: ContextualDatabase?
|
||||
) {
|
||||
if (isResultLauncherRegistered.not()) {
|
||||
isResultLauncherRegistered = true
|
||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||
if (e is PrivilegedAllowLists.PrivilegedException) {
|
||||
showAppPrivilegedDialog(e.temptingApp)
|
||||
} else {
|
||||
showError(e)
|
||||
}
|
||||
}) {
|
||||
launchPasskeyAction(intent, specialMode, database)
|
||||
}
|
||||
// Launch with database when a nodeId is present
|
||||
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
|
||||
super.launchActionIfNeeded(intent, specialMode, database)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the main action to manage Passkey
|
||||
*/
|
||||
private suspend fun launchPasskeyAction(
|
||||
override suspend fun launchAction(
|
||||
intent: Intent,
|
||||
specialMode: SpecialMode,
|
||||
database: ContextualDatabase?
|
||||
@@ -194,6 +168,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
|
||||
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
|
||||
val nodeId = intent.retrieveNodeId()
|
||||
intent.removeInfo()
|
||||
intent.removeAppOrigin()
|
||||
intent.removeNodeId()
|
||||
checkSecurity(intent, nodeId)
|
||||
when (specialMode) {
|
||||
SpecialMode.SELECTION -> {
|
||||
@@ -260,15 +237,19 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
TAG, "No Passkey found for selection," +
|
||||
"launch manual selection in opened database"
|
||||
)
|
||||
_uiState.value = UIState.LaunchGroupActivityForSelection(
|
||||
database = openedDatabase
|
||||
)
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForSelection(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
Log.d(TAG, "Manual passkey selection in closed database")
|
||||
_uiState.value =
|
||||
UIState.LaunchFileDatabaseSelectActivityForSelection(
|
||||
searchInfo = searchInfo
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchFileDatabaseSelectActivityForSelection(
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -326,12 +307,12 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
appOrigin = appOrigin
|
||||
),
|
||||
passkey = passkey,
|
||||
backupEligibility = mBackupEligibility,
|
||||
backupState = mBackupState
|
||||
defaultBackupEligibility = mBackupEligibility,
|
||||
defaultBackupState = mBackupState
|
||||
)
|
||||
)
|
||||
)
|
||||
setResult(result)
|
||||
setResult(result, lockDatabase = mLockDatabaseAfterSelection)
|
||||
} catch (e: SignatureNotFoundException) {
|
||||
// Request the dialog if signature exception
|
||||
showAppSignatureDialog(e.temptingApp, nodeId)
|
||||
@@ -340,9 +321,11 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
}
|
||||
}
|
||||
|
||||
fun manageSelectionResult(
|
||||
override fun manageSelectionResult(
|
||||
database: ContextualDatabase,
|
||||
activityResult: ActivityResult
|
||||
) {
|
||||
super.manageSelectionResult(database, activityResult)
|
||||
val intent = activityResult.data
|
||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||
Log.e(TAG, "Unable to create selection response for passkey", e)
|
||||
@@ -380,8 +363,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
appOrigin = appOrigin
|
||||
),
|
||||
passkey = passkey,
|
||||
backupEligibility = mBackupEligibility,
|
||||
backupState = mBackupState
|
||||
defaultBackupEligibility = mBackupEligibility,
|
||||
defaultBackupState = mBackupState
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -389,7 +372,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
throw IOException("Usage parameters is null")
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
setResult(responseIntent)
|
||||
setResult(responseIntent, lockDatabase = mLockDatabaseAfterSelection)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,6 +400,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
retrievePasskeyCreationRequestParameters(
|
||||
intent = intent,
|
||||
context = getApplication(),
|
||||
defaultBackupEligibility = mBackupEligibility,
|
||||
defaultBackupState = mBackupState,
|
||||
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
|
||||
// Save the requested parameters
|
||||
mPasskey = passkey
|
||||
@@ -440,24 +425,26 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
TAG, "Passkey found for registration, " +
|
||||
"but launch manual registration for a new entry"
|
||||
)
|
||||
_uiState.value = UIState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
Log.d(TAG, "Launch new manual registration in opened database")
|
||||
_uiState.value = UIState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
Log.d(TAG, "Manual passkey registration in closed database")
|
||||
_uiState.value =
|
||||
UIState.LaunchFileDatabaseSelectActivityForRegistration(
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
@@ -490,7 +477,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
}
|
||||
}
|
||||
|
||||
fun manageRegistrationResult(activityResult: ActivityResult) {
|
||||
override fun manageRegistrationResult(activityResult: ActivityResult) {
|
||||
val intent = activityResult.data
|
||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||
Log.e(TAG, "Unable to create registration response for passkey", e)
|
||||
@@ -518,8 +505,10 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
intent = responseIntent,
|
||||
response = buildCreatePublicKeyCredentialResponse(
|
||||
publicKeyCredentialCreationParameters = it,
|
||||
backupEligibility = mBackupEligibility,
|
||||
backupState = mBackupState
|
||||
backupEligibility = passkey?.backupEligibility
|
||||
?: mBackupEligibility,
|
||||
backupState = passkey?.backupState
|
||||
?: mBackupState
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -549,29 +538,6 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
||||
val temptingApp: AppOrigin,
|
||||
val nodeId: UUID
|
||||
): UIState()
|
||||
data class LaunchGroupActivityForSelection(
|
||||
val database: ContextualDatabase
|
||||
): UIState()
|
||||
data class LaunchGroupActivityForRegistration(
|
||||
val database: ContextualDatabase,
|
||||
val registerInfo: RegisterInfo,
|
||||
val typeMode: TypeMode
|
||||
): UIState()
|
||||
data class LaunchFileDatabaseSelectActivityForSelection(
|
||||
val searchInfo: SearchInfo
|
||||
): UIState()
|
||||
data class LaunchFileDatabaseSelectActivityForRegistration(
|
||||
val registerInfo: RegisterInfo,
|
||||
val typeMode: TypeMode
|
||||
): UIState()
|
||||
data class SetActivityResult(
|
||||
val lockDatabase: Boolean,
|
||||
val resultCode: Int,
|
||||
val data: Intent? = null
|
||||
): UIState()
|
||||
data class ShowError(
|
||||
val error: Throwable
|
||||
): UIState()
|
||||
data class UpdateEntry(
|
||||
val oldEntry: Entry,
|
||||
val newEntry: Entry
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database
|
||||
|
||||
import android.Manifest
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
@@ -29,23 +28,15 @@ import android.content.Context.BIND_IMPORTANT
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
@@ -55,7 +46,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
|
||||
@@ -89,13 +79,9 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.putParcelableList
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -103,121 +89,29 @@ import java.util.UUID
|
||||
* Useful to retrieve a database instance and sending tasks commands
|
||||
*/
|
||||
class DatabaseTaskProvider(
|
||||
private var context: Context,
|
||||
private var showDialog: Boolean = true
|
||||
private var context: Context
|
||||
) {
|
||||
|
||||
// To show dialog only if context is an activity
|
||||
private var activity: FragmentActivity? = try {
|
||||
context as? FragmentActivity?
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
||||
|
||||
var onActionFinish: ((
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) -> Unit)? = null
|
||||
|
||||
private var intentDatabaseTask: Intent = Intent(
|
||||
context.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java
|
||||
)
|
||||
var onStartActionRequested: ((bundle: Bundle?, actionTask: String) -> Unit)? = null
|
||||
var actionTaskListener: DatabaseTaskNotificationService.ActionTaskListener? = null
|
||||
var databaseInfoListener: DatabaseTaskNotificationService.DatabaseInfoListener? = null
|
||||
|
||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||
|
||||
private var serviceConnection: ServiceConnection? = null
|
||||
|
||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||
|
||||
fun destroy() {
|
||||
this.activity = null
|
||||
this.onDatabaseRetrieved = null
|
||||
this.onActionFinish = null
|
||||
this.databaseTaskBroadcastReceiver = null
|
||||
this.mBinder = null
|
||||
this.serviceConnection = null
|
||||
this.progressTaskDialogFragment = null
|
||||
this.databaseChangedDialogFragment = null
|
||||
}
|
||||
|
||||
private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
|
||||
override fun onActionStarted(
|
||||
database: ContextualDatabase,
|
||||
progressMessage: ProgressMessage
|
||||
) {
|
||||
if (showDialog)
|
||||
startDialog(progressMessage)
|
||||
}
|
||||
|
||||
override fun onActionUpdated(
|
||||
database: ContextualDatabase,
|
||||
progressMessage: ProgressMessage
|
||||
) {
|
||||
if (showDialog)
|
||||
updateDialog(progressMessage)
|
||||
}
|
||||
|
||||
override fun onActionStopped(
|
||||
database: ContextualDatabase
|
||||
) {
|
||||
// Remove the progress task
|
||||
stopDialog()
|
||||
}
|
||||
|
||||
override fun onActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
onActionFinish?.invoke(database, actionTask, result)
|
||||
onActionStopped(database)
|
||||
}
|
||||
}
|
||||
|
||||
private val mActionDatabaseListener =
|
||||
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||
override fun validateDatabaseChanged() {
|
||||
mBinder?.getService()?.saveDatabaseInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private var databaseInfoListener = object :
|
||||
DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||
override fun onDatabaseInfoChanged(
|
||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnlyDatabase: Boolean
|
||||
) {
|
||||
activity?.let { activity ->
|
||||
activity.lifecycleScope.launch {
|
||||
if (databaseChangedDialogFragment == null) {
|
||||
databaseChangedDialogFragment = activity.supportFragmentManager
|
||||
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||
previousDatabaseInfo,
|
||||
newDatabaseInfo,
|
||||
readOnlyDatabase
|
||||
)
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
databaseChangedDialogFragment?.show(
|
||||
activity.supportFragmentManager,
|
||||
DATABASE_CHANGED_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fun onDatabaseChangeValidated() {
|
||||
mBinder?.getService()?.saveDatabaseInfo()
|
||||
}
|
||||
|
||||
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
|
||||
@@ -226,48 +120,17 @@ class DatabaseTaskProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDialog(progressMessage: ProgressMessage) {
|
||||
activity?.let { activity ->
|
||||
activity.lifecycleScope.launch {
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = activity.supportFragmentManager
|
||||
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||
progressTaskDialogFragment?.show(
|
||||
activity.supportFragmentManager,
|
||||
PROGRESS_TASK_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
updateDialog(progressMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDialog(progressMessage: ProgressMessage) {
|
||||
progressTaskDialogFragment?.apply {
|
||||
updateTitle(progressMessage.titleId)
|
||||
updateMessage(progressMessage.messageId)
|
||||
updateWarning(progressMessage.warningId)
|
||||
setCancellable(progressMessage.cancelable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopDialog() {
|
||||
progressTaskDialogFragment?.dismissAllowingStateLoss()
|
||||
progressTaskDialogFragment = null
|
||||
}
|
||||
|
||||
private fun initServiceConnection() {
|
||||
if (serviceConnection == null) {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
stopDialog()
|
||||
actionTaskListener?.onActionStopped()
|
||||
onDatabaseRetrieved?.invoke(null)
|
||||
}
|
||||
|
||||
override fun onNullBinding(name: ComponentName?) {
|
||||
stopDialog()
|
||||
actionTaskListener?.onActionStopped()
|
||||
onDatabaseRetrieved?.invoke(null)
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
@@ -290,21 +153,33 @@ class DatabaseTaskProvider(
|
||||
|
||||
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||
service?.addDatabaseListener(databaseListener)
|
||||
service?.addDatabaseFileInfoListener(databaseInfoListener)
|
||||
service?.addActionTaskListener(actionTaskListener)
|
||||
databaseInfoListener?.let { infoListener ->
|
||||
service?.addDatabaseFileInfoListener(infoListener)
|
||||
}
|
||||
actionTaskListener?.let { taskListener ->
|
||||
service?.addActionTaskListener(taskListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||
service?.removeActionTaskListener(actionTaskListener)
|
||||
service?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
actionTaskListener?.let { taskListener ->
|
||||
service?.removeActionTaskListener(taskListener)
|
||||
}
|
||||
databaseInfoListener?.let { infoListener ->
|
||||
service?.removeDatabaseFileInfoListener(infoListener)
|
||||
}
|
||||
service?.removeDatabaseListener(databaseListener)
|
||||
onDatabaseRetrieved?.invoke(null)
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
initServiceConnection()
|
||||
serviceConnection?.let {
|
||||
context.bindService(
|
||||
intentDatabaseTask,
|
||||
Intent(
|
||||
context.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java
|
||||
),
|
||||
it,
|
||||
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
|
||||
)
|
||||
@@ -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) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val contextActivity = activity
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
startService(bundle, actionTask)
|
||||
} else if (contextActivity != null && shouldShowRequestPermissionRationale(
|
||||
contextActivity,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
) {
|
||||
// it's not the first time, so the user deliberately chooses not to display the notification
|
||||
startService(bundle, actionTask)
|
||||
} else {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.warning_database_notification_permission)
|
||||
.setNegativeButton(R.string.later) { _, _ ->
|
||||
// Refuses the notification, so start the service
|
||||
startService(bundle, actionTask)
|
||||
}
|
||||
.setPositiveButton(R.string.ask) { _, _ ->
|
||||
// Save the temp parameters to ask the permission
|
||||
tempServiceParameters.add(Pair(bundle, actionTask))
|
||||
requestPermissionLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}.create().show()
|
||||
}
|
||||
} else {
|
||||
startService(bundle, actionTask)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startService(bundle: Bundle? = null, actionTask: String) {
|
||||
try {
|
||||
if (bundle != null)
|
||||
intentDatabaseTask.putExtras(bundle)
|
||||
intentDatabaseTask.action = actionTask
|
||||
context.startService(intentDatabaseTask)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to perform database action", e)
|
||||
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||
onStartActionRequested?.invoke(bundle, actionTask) ?: run {
|
||||
context.startDatabaseService(bundle, actionTask)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -842,5 +668,21 @@ class DatabaseTaskProvider(
|
||||
|
||||
companion object {
|
||||
private val TAG = DatabaseTaskProvider::class.java.name
|
||||
|
||||
fun Context.startDatabaseService(bundle: Bundle? = null, actionTask: String) {
|
||||
try {
|
||||
val intentDatabaseTask = Intent(
|
||||
applicationContext,
|
||||
DatabaseTaskNotificationService::class.java
|
||||
)
|
||||
if (bundle != null)
|
||||
intentDatabaseTask.putExtras(bundle)
|
||||
intentDatabaseTask.action = actionTask
|
||||
startService(intentDatabaseTask)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to perform database action", e)
|
||||
Toast.makeText(this, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.StringRes
|
||||
|
||||
data class ProgressMessage(
|
||||
@StringRes
|
||||
var titleId: Int,
|
||||
var titleId: Int? = null,
|
||||
@StringRes
|
||||
var messageId: Int? = null,
|
||||
@StringRes
|
||||
|
||||
@@ -47,6 +47,8 @@ import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BE
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BS
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
|
||||
@@ -146,6 +148,8 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
||||
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
|
||||
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
|
||||
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
|
||||
FIELD_FLAG_BE.equals(name, true) -> context.getString(R.string.passkey_backup_eligibility)
|
||||
FIELD_FLAG_BS.equals(name, true) -> context.getString(R.string.passkey_backup_state)
|
||||
|
||||
else -> name
|
||||
}
|
||||
|
||||
@@ -21,9 +21,16 @@ package com.kunzisoft.keepass.database.helper
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||
|
||||
object SearchHelper {
|
||||
|
||||
@@ -40,6 +47,76 @@ object SearchHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the concrete web domain AKA without sub domain if needed
|
||||
*/
|
||||
private fun getConcreteWebDomain(
|
||||
context: Context,
|
||||
webDomain: String?,
|
||||
concreteWebDomain: (searchSubDomains: Boolean, concreteWebDomain: String?) -> Unit
|
||||
) {
|
||||
val domain = webDomain
|
||||
val searchSubDomains = searchSubDomains(context)
|
||||
if (domain != null) {
|
||||
// Warning, web domain can contains IP, don't crop in this case
|
||||
if (searchSubDomains
|
||||
|| Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
|
||||
concreteWebDomain.invoke(searchSubDomains, webDomain)
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val publicSuffixList = PublicSuffixList(context)
|
||||
val publicSuffix = publicSuffixList
|
||||
.getPublicSuffixPlusOne(domain).await()
|
||||
withContext(Dispatchers.Main) {
|
||||
concreteWebDomain.invoke(false, publicSuffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
concreteWebDomain.invoke(searchSubDomains, null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create search parameters asynchronously from [SearchInfo]
|
||||
*/
|
||||
fun SearchInfo.getSearchParametersFromSearchInfo(
|
||||
context: Context,
|
||||
callback: (SearchParameters) -> Unit
|
||||
) {
|
||||
getConcreteWebDomain(
|
||||
context,
|
||||
webDomain
|
||||
) { searchSubDomains, concreteDomain ->
|
||||
var query = this.toString()
|
||||
if (isDomainSearch && concreteDomain != null)
|
||||
query = concreteDomain
|
||||
callback.invoke(
|
||||
SearchParameters().apply {
|
||||
searchQuery = query
|
||||
allowEmptyQuery = false
|
||||
searchInTitles = false
|
||||
searchInUsernames = false
|
||||
searchInPasswords = false
|
||||
searchInAppIds = isAppIdSearch
|
||||
searchInUrls = isDomainSearch
|
||||
searchByDomain = true
|
||||
searchBySubDomain = searchSubDomains
|
||||
searchInRelyingParty = isPasskeySearch
|
||||
searchInNotes = false
|
||||
searchInOTP = isOTPSearch
|
||||
searchInOther = false
|
||||
searchInUUIDs = false
|
||||
searchInTags = isTagSearch
|
||||
searchInCurrentGroup = false
|
||||
searchInSearchableGroup = true
|
||||
searchInRecycleBin = false
|
||||
searchInTemplates = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||
*/
|
||||
@@ -52,28 +129,31 @@ object SearchHelper {
|
||||
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
||||
onDatabaseClosed: () -> Unit
|
||||
) {
|
||||
// Do not place coroutine at start, bug in Passkey implementation
|
||||
if (database == null || !database.loaded) {
|
||||
onDatabaseClosed.invoke()
|
||||
} else if (TimeoutHelper.checkTime(context)) {
|
||||
var searchWithoutUI = false
|
||||
if (searchInfo != null
|
||||
&& !searchInfo.manualSelection
|
||||
&& !searchInfo.containsOnlyNullValues()) {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
searchInfo,
|
||||
MAX_SEARCH_ENTRY
|
||||
)?.let { searchGroup ->
|
||||
if (searchGroup.numberOfChildEntries > 0) {
|
||||
searchWithoutUI = true
|
||||
onItemsFound.invoke(database,
|
||||
searchGroup.getChildEntriesInfo(database))
|
||||
}
|
||||
&& !searchInfo.containsOnlyNullValues()
|
||||
) {
|
||||
searchInfo.getSearchParametersFromSearchInfo(context) { searchParameters ->
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
searchParameters = searchParameters,
|
||||
max = MAX_SEARCH_ENTRY
|
||||
)?.let { searchGroup ->
|
||||
if (searchGroup.numberOfChildEntries > 0) {
|
||||
onItemsFound.invoke(
|
||||
database,
|
||||
searchGroup.getChildEntriesInfo(database)
|
||||
)
|
||||
} else
|
||||
onItemNotFound.invoke(database)
|
||||
} ?: onItemNotFound.invoke(database)
|
||||
}
|
||||
}
|
||||
if (!searchWithoutUI) {
|
||||
} else
|
||||
onItemNotFound.invoke(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun generatePassword(length: Int,
|
||||
upperCase: Boolean,
|
||||
lowerCase: Boolean,
|
||||
digits: Boolean,
|
||||
minus: Boolean,
|
||||
underline: Boolean,
|
||||
space: Boolean,
|
||||
specials: Boolean,
|
||||
brackets: Boolean,
|
||||
extended: Boolean,
|
||||
considerChars: String,
|
||||
ignoreChars: String,
|
||||
atLeastOneFromEach: Boolean,
|
||||
excludeAmbiguousChar: Boolean): String {
|
||||
fun generatePassword(
|
||||
length: Int,
|
||||
upperCase: Boolean,
|
||||
lowerCase: Boolean,
|
||||
digits: Boolean,
|
||||
minus: Boolean,
|
||||
underline: Boolean,
|
||||
space: Boolean,
|
||||
specials: Boolean,
|
||||
brackets: Boolean,
|
||||
extended: Boolean,
|
||||
considerChars: String,
|
||||
ignoreChars: String,
|
||||
atLeastOneFromEach: Boolean,
|
||||
excludeAmbiguousChar: Boolean
|
||||
): String {
|
||||
// Desired password length is 0 or less
|
||||
if (length <= 0) {
|
||||
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
|
||||
@@ -228,7 +230,7 @@ class PasswordGenerator(private val resources: Resources) {
|
||||
private const val MINUS_CHAR = "-"
|
||||
private const val UNDERLINE_CHAR = "_"
|
||||
private const val SPACE_CHAR = " "
|
||||
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
|
||||
private const val SPECIAL_CHARS = "&/,^@.#:%\\='$!?*`;+\"|~"
|
||||
private const val BRACKET_CHARS = "[]{}()<>"
|
||||
private const val AMBIGUOUS_CHARS = "iI|lLoO01"
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -164,7 +165,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
}
|
||||
}
|
||||
if (attachmentNotificationList.isEmpty()) {
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,7 +195,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
||||
|
||||
val pendingContentIntent = PendingIntent.getActivity(this,
|
||||
0,
|
||||
randomRequestCode(),
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(attachmentNotification.uri,
|
||||
@@ -208,7 +209,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
)
|
||||
|
||||
val pendingDeleteIntent = PendingIntent.getService(this,
|
||||
0,
|
||||
randomRequestCode(),
|
||||
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
||||
// No action to delete the service
|
||||
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||
|
||||
@@ -62,7 +62,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
// Stop the service
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
||||
@@ -61,13 +61,14 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
@@ -175,7 +176,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
progressMessage: ProgressMessage
|
||||
)
|
||||
fun onActionStopped(
|
||||
database: ContextualDatabase
|
||||
database: ContextualDatabase? = null
|
||||
)
|
||||
fun onActionFinished(
|
||||
database: ContextualDatabase,
|
||||
@@ -261,11 +262,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
)
|
||||
}
|
||||
} else {
|
||||
/* Do not stopped here, service cannot be connected
|
||||
mActionTaskListeners.forEach { actionTaskListener ->
|
||||
actionTaskListener.onActionStopped(
|
||||
database
|
||||
)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,7 +339,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
val intentAction = intent?.action
|
||||
|
||||
if (intentAction == null && !database.loaded) {
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
val actionRunnable: ActionRunnable? = when (intentAction) {
|
||||
@@ -446,10 +448,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||
// Stop service after save if user remove task
|
||||
if (save && mTaskRemovedRequested) {
|
||||
actionOnLock()
|
||||
stopService()
|
||||
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
|
||||
if (!database.loaded) {
|
||||
stopSelf()
|
||||
stopService()
|
||||
} else {
|
||||
// Restart the service to open lock notification
|
||||
try {
|
||||
@@ -534,11 +536,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
|
||||
val notificationBuilder = buildNewNotification().apply {
|
||||
setSmallIcon(iconId)
|
||||
intent?.let {
|
||||
setContentTitle(getString(
|
||||
intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId))
|
||||
)
|
||||
}
|
||||
val titleId = mProgressMessage.titleId?.let {
|
||||
intent?.getIntExtra(DATABASE_TASK_TITLE_KEY, it)
|
||||
} ?: R.string.app_name
|
||||
setContentTitle(getString(titleId))
|
||||
setAutoCancel(false)
|
||||
setContentIntent(null)
|
||||
}
|
||||
@@ -550,7 +551,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
// Build Intents for notification action
|
||||
val pendingDatabaseIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
randomRequestCode(),
|
||||
Intent(this, GroupActivity::class.java),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
@@ -660,7 +661,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
|
||||
private fun updateMessage(resId: Int) {
|
||||
mProgressMessage.messageId = resId
|
||||
mProgressMessage = mProgressMessage.copy(
|
||||
messageId = resId
|
||||
)
|
||||
notifyProgressMessage()
|
||||
}
|
||||
|
||||
@@ -672,13 +675,19 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
updateMessage(R.string.decrypting_db)
|
||||
}
|
||||
|
||||
override fun actionOnLock() {
|
||||
override fun stopService() {
|
||||
if (!TimeoutHelper.temporarilyDisableLock) {
|
||||
closeDatabase(mDatabase)
|
||||
// Remove the database during the lock
|
||||
// And notify each subscriber
|
||||
mDatabase = null
|
||||
mDatabaseListeners.forEach { listener ->
|
||||
listener.onDatabaseRetrieved(null)
|
||||
}
|
||||
// Remove the lock timer (no more needed if it exists)
|
||||
TimeoutHelper.cancelLockTimer(this)
|
||||
// Service is stopped after receive the broadcast
|
||||
super.actionOnLock()
|
||||
super.stopService()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,9 +718,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
notifyProgressMessage()
|
||||
HardwareKeyActivity
|
||||
.launchHardwareKeyActivity(
|
||||
this@DatabaseTaskNotificationService,
|
||||
hardwareKey,
|
||||
seed
|
||||
context = this@DatabaseTaskNotificationService,
|
||||
hardwareKey = hardwareKey,
|
||||
seed = seed
|
||||
)
|
||||
// Wait the response
|
||||
mProgressMessage.apply {
|
||||
@@ -722,7 +731,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
// Close channels
|
||||
closeChallengeResponse()
|
||||
// Restore previous message
|
||||
mProgressMessage = previousMessage
|
||||
mProgressMessage = previousMessage.apply {
|
||||
cancelable = null
|
||||
}
|
||||
notifyProgressMessage()
|
||||
}
|
||||
return response
|
||||
|
||||
@@ -55,7 +55,7 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
// Stop the service
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.services
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.app.ServiceCompat
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LockReceiver
|
||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||
@@ -29,13 +28,7 @@ import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
||||
abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
private var mLockReceiver: LockReceiver = LockReceiver {
|
||||
actionOnLock()
|
||||
}
|
||||
|
||||
protected open fun actionOnLock() {
|
||||
// Stop the service in all cases
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -46,7 +39,7 @@ abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
if (!TimeoutHelper.temporarilyDisableLock) {
|
||||
actionOnLock()
|
||||
stopService()
|
||||
}
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.util.TypedValue
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
@@ -114,6 +115,12 @@ abstract class NotificationService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun stopService() {
|
||||
// Stop the service in all cases
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
protected fun defineTimerJob(builder: NotificationCompat.Builder,
|
||||
type: NotificationServiceType,
|
||||
timeoutMilliseconds: Long,
|
||||
|
||||
@@ -41,6 +41,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
autofillInlineSuggestionsPreference?.isVisible = false
|
||||
}
|
||||
|
||||
val autofillAskSaveDataPreference: TwoStatePreference? = findPreference(getString(R.string.autofill_ask_to_save_data_key))
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
autofillAskSaveDataPreference?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
|
||||
@@ -13,6 +13,8 @@ abstract class ExternalSettingsActivity : DatabaseModeActivity() {
|
||||
|
||||
private var lockView: FloatingActionButton? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
||||
@@ -21,20 +21,25 @@ package com.kunzisoft.keepass.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private var mCallback: Callback? = null
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabaseLoaded: Boolean = false
|
||||
private val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
@@ -50,20 +55,24 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
mCallback = null
|
||||
super.onDetach()
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
||||
mDatabaseLoaded = database?.loaded == true
|
||||
checkDatabaseLoaded()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
checkDatabaseLoaded(database?.loaded == true)
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
private fun checkDatabaseLoaded() {
|
||||
private fun checkDatabaseLoaded(isDatabaseLoaded: Boolean) {
|
||||
findPreference<Preference>(getString(R.string.settings_database_key))
|
||||
?.isEnabled = mDatabaseLoaded
|
||||
?.isEnabled = isDatabaseLoaded
|
||||
|
||||
findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key))
|
||||
?.isVisible = mDatabaseLoaded
|
||||
?.isVisible = isDatabaseLoaded
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@@ -119,7 +128,7 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
checkDatabaseLoaded()
|
||||
checkDatabaseLoaded(mDatabase?.loaded == true)
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
||||
@@ -19,13 +19,21 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.TwoStatePreference
|
||||
@@ -39,19 +47,40 @@ import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.helper.*
|
||||
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.settings.preference.*
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
|
||||
import com.kunzisoft.keepass.settings.preference.DialogColorPreference
|
||||
import com.kunzisoft.keepass.settings.preference.DialogListExplanationPreference
|
||||
import com.kunzisoft.keepass.settings.preference.InputKdfNumberPreference
|
||||
import com.kunzisoft.keepass.settings.preference.InputKdfSizePreference
|
||||
import com.kunzisoft.keepass.settings.preference.InputNumberPreference
|
||||
import com.kunzisoft.keepass.settings.preference.InputTextPreference
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseColorPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDataCompressionPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDefaultUsernamePreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDescriptionPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistorySizePreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMemoryUsagePreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseNamePreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseParallelismPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRoundsPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseTemplatesGroupPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getSerializableCompat
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabase: ContextualDatabase? = null
|
||||
private val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
private var mDatabaseReadOnly: Boolean = false
|
||||
private var mMergeDataAllowed: Boolean = false
|
||||
private var mDatabaseAutoSaveEnabled: Boolean = true
|
||||
@@ -114,19 +143,46 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||
|
||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
||||
mDatabase = database
|
||||
view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded)
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
|
||||
onDatabaseActionFinished(it.database, it.actionTask, it.result)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
view.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||
context = requireContext(),
|
||||
databaseLoaded = database?.loaded
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,29 +223,26 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
mDatabaseViewModel.reloadDatabase(false)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
mDatabase = database
|
||||
mDatabaseReadOnly = database?.isReadOnly == true
|
||||
mMergeDataAllowed = database?.isMergeDataAllowed() == true
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
mDatabaseReadOnly = database.isReadOnly
|
||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||
|
||||
mDatabase?.let {
|
||||
if (it.loaded) {
|
||||
when (mScreen) {
|
||||
Screen.DATABASE -> {
|
||||
onCreateDatabasePreference(it)
|
||||
}
|
||||
Screen.DATABASE_SECURITY -> {
|
||||
onCreateDatabaseSecurityPreference(it)
|
||||
}
|
||||
Screen.DATABASE_MASTER_KEY -> {
|
||||
onCreateDatabaseMasterKeyPreference(it)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
if (database.loaded) {
|
||||
when (mScreen) {
|
||||
Screen.DATABASE -> {
|
||||
onCreateDatabasePreference(database)
|
||||
}
|
||||
Screen.DATABASE_SECURITY -> {
|
||||
onCreateDatabaseSecurityPreference(database)
|
||||
}
|
||||
Screen.DATABASE_MASTER_KEY -> {
|
||||
onCreateDatabaseMasterKeyPreference(database)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
} else {
|
||||
Log.e(javaClass.name, "Database isn't ready")
|
||||
}
|
||||
} else {
|
||||
Log.e(javaClass.name, "Database isn't ready")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,7 +511,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newDefaultUsername
|
||||
} else {
|
||||
mDatabase?.defaultUsername = oldDefaultUsername
|
||||
database.defaultUsername = oldDefaultUsername
|
||||
oldDefaultUsername
|
||||
}
|
||||
dbDefaultUsernamePref?.summary = defaultUsernameToShow
|
||||
@@ -471,7 +524,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newColor
|
||||
} else {
|
||||
mDatabase?.customColor = Color.parseColor(oldColor)
|
||||
database.customColor = oldColor.toColorInt()
|
||||
oldColor
|
||||
}
|
||||
dbCustomColorPref?.summary = defaultColorToShow
|
||||
@@ -483,7 +536,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newCompression
|
||||
} else {
|
||||
mDatabase?.compressionAlgorithm = oldCompression
|
||||
database.compressionAlgorithm = oldCompression
|
||||
oldCompression
|
||||
}
|
||||
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
|
||||
@@ -497,7 +550,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
} else {
|
||||
oldRecycleBin
|
||||
}
|
||||
mDatabase?.setRecycleBin(recycleBinToShow)
|
||||
database.setRecycleBin(recycleBinToShow)
|
||||
refreshRecycleBinGroup(database)
|
||||
}
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
|
||||
@@ -509,7 +562,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
} else {
|
||||
oldTemplatesGroup
|
||||
}
|
||||
mDatabase?.setTemplatesGroup(templatesGroupToShow)
|
||||
database.setTemplatesGroup(templatesGroupToShow)
|
||||
refreshTemplatesGroup(database)
|
||||
}
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
|
||||
@@ -519,7 +572,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newMaxHistoryItems
|
||||
} else {
|
||||
mDatabase?.historyMaxItems = oldMaxHistoryItems
|
||||
database.historyMaxItems = oldMaxHistoryItems
|
||||
oldMaxHistoryItems
|
||||
}
|
||||
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
|
||||
@@ -531,7 +584,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newMaxHistorySize
|
||||
} else {
|
||||
mDatabase?.historyMaxSize = oldMaxHistorySize
|
||||
database.historyMaxSize = oldMaxHistorySize
|
||||
oldMaxHistorySize
|
||||
}
|
||||
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
|
||||
@@ -549,7 +602,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newEncryption
|
||||
} else {
|
||||
mDatabase?.encryptionAlgorithm = oldEncryption
|
||||
database.encryptionAlgorithm = oldEncryption
|
||||
oldEncryption
|
||||
}
|
||||
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
|
||||
@@ -561,7 +614,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newKeyDerivationEngine
|
||||
} else {
|
||||
mDatabase?.kdfEngine = oldKeyDerivationEngine
|
||||
database.kdfEngine = oldKeyDerivationEngine
|
||||
oldKeyDerivationEngine
|
||||
}
|
||||
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
|
||||
@@ -578,7 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newIterations
|
||||
} else {
|
||||
mDatabase?.numberKeyEncryptionRounds = oldIterations
|
||||
database.numberKeyEncryptionRounds = oldIterations
|
||||
oldIterations
|
||||
}
|
||||
mRoundPref?.summary = roundsToShow.toString()
|
||||
@@ -590,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newMemoryUsage
|
||||
} else {
|
||||
mDatabase?.memoryUsage = oldMemoryUsage
|
||||
database.memoryUsage = oldMemoryUsage
|
||||
oldMemoryUsage
|
||||
}
|
||||
mMemoryPref?.summary = memoryToShow.toString()
|
||||
@@ -602,7 +655,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newParallelism
|
||||
} else {
|
||||
mDatabase?.parallelism = oldParallelism
|
||||
database.parallelism = oldParallelism
|
||||
oldParallelism
|
||||
}
|
||||
mParallelismPref?.summary = parallelismToShow.toString()
|
||||
|
||||
@@ -108,7 +108,7 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.auto_focus_search_default))
|
||||
}
|
||||
|
||||
fun searchSubdomains(context: Context): Boolean {
|
||||
fun searchSubDomains(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
|
||||
context.resources.getBoolean(R.bool.subdomain_search_default))
|
||||
@@ -352,6 +352,8 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.search_option_username_default))
|
||||
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
|
||||
context.resources.getBoolean(R.bool.search_option_password_default))
|
||||
searchInAppIds = prefs.getBoolean(context.getString(R.string.search_option_application_id_key),
|
||||
context.resources.getBoolean(R.bool.search_option_application_id_default))
|
||||
searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
|
||||
context.resources.getBoolean(R.bool.search_option_url_default))
|
||||
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
|
||||
@@ -389,6 +391,8 @@ object PreferencesUtil {
|
||||
searchParameters.searchInUsernames)
|
||||
putBoolean(context.getString(R.string.search_option_password_key),
|
||||
searchParameters.searchInPasswords)
|
||||
putBoolean(context.getString(R.string.search_option_application_id_key),
|
||||
searchParameters.searchInAppIds)
|
||||
putBoolean(context.getString(R.string.search_option_url_key),
|
||||
searchParameters.searchInUrls)
|
||||
putBoolean(context.getString(R.string.search_option_expired_key),
|
||||
@@ -686,6 +690,12 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
|
||||
}
|
||||
|
||||
fun isPasskeyCloseDatabaseEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.passkeys_close_database_key),
|
||||
context.resources.getBoolean(R.bool.passkeys_close_database_default))
|
||||
}
|
||||
|
||||
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
|
||||
@@ -854,6 +864,10 @@ object PreferencesUtil {
|
||||
context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.passkeys_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.passkeys_auto_select_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.passkeys_backup_eligibility_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.passkeys_backup_state_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
|
||||
@@ -70,8 +70,12 @@ open class SettingsActivity
|
||||
// To apply navigation bar with background color
|
||||
/* TODO Settings nav bar
|
||||
setTransparentNavigationBar {
|
||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
||||
coordinatorLayout?.applyWindowInsets(EnumSet.of(
|
||||
WindowInsetPosition.TOP_MARGINS,
|
||||
WindowInsetPosition.BOTTOM_MARGINS,
|
||||
WindowInsetPosition.START_MARGINS,
|
||||
WindowInsetPosition.END_MARGINS,
|
||||
))
|
||||
}*/
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
@@ -155,10 +159,6 @@ open class SettingsActivity
|
||||
return coordinatorLayout
|
||||
}
|
||||
|
||||
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
@@ -188,7 +188,7 @@ open class SettingsActivity
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||
assignPassword(mainCredential)
|
||||
assignMainCredential(mainCredential)
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||
|
||||
@@ -95,20 +95,16 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
database?.let {
|
||||
var initColor = it.customColor
|
||||
if (initColor != null) {
|
||||
enableSwitchView.isChecked = true
|
||||
} else {
|
||||
enableSwitchView.isChecked = false
|
||||
initColor = DEFAULT_COLOR
|
||||
}
|
||||
chromaColorView.currentColor = initColor
|
||||
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
var initColor = database.customColor
|
||||
if (initColor != null) {
|
||||
enableSwitchView.isChecked = true
|
||||
} else {
|
||||
enableSwitchView.isChecked = false
|
||||
initColor = DEFAULT_COLOR
|
||||
}
|
||||
chromaColorView.currentColor = initColor
|
||||
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -50,16 +50,14 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
setExplanationText(R.string.database_data_compression_summary)
|
||||
|
||||
mRecyclerView?.adapter = mCompressionAdapter
|
||||
|
||||
database?.let {
|
||||
compressionSelected = it.compressionAlgorithm
|
||||
mCompressionAdapter?.setItems(it.availableCompressionAlgorithms, compressionSelected)
|
||||
}
|
||||
compressionSelected = database.compressionAlgorithm
|
||||
mCompressionAdapter?.setItems(
|
||||
items = database.availableCompressionAlgorithms,
|
||||
itemUsed = compressionSelected
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
|
||||
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
inputText = database?.defaultUsername?: ""
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
inputText = database.defaultUsername
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
|
||||
class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
inputText = database?.description ?: ""
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
inputText = database.description
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -51,12 +51,9 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
algorithmSelected = database.encryptionAlgorithm
|
||||
mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
algorithmSelected = database.encryptionAlgorithm
|
||||
mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -54,12 +54,12 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
kdfEngineSelected = database.kdfEngine
|
||||
mKdfAdapter?.setItems(database.availableKdfEngines, kdfEngineSelected)
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
kdfEngineSelected = database.kdfEngine
|
||||
mKdfAdapter?.setItems(
|
||||
items = database.availableKdfEngines,
|
||||
itemUsed = kdfEngineSelected
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -31,19 +31,17 @@ class DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePrefer
|
||||
setExplanationText(R.string.max_history_items_summary)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.historyMaxItems?.let { maxItemsDatabase ->
|
||||
inputText = maxItemsDatabase.toString()
|
||||
setSwitchAction({ isChecked ->
|
||||
inputText = if (!isChecked) {
|
||||
NONE_MAX_HISTORY_ITEMS.toString()
|
||||
} else {
|
||||
DEFAULT_MAX_HISTORY_ITEMS.toString()
|
||||
}
|
||||
showInputText(isChecked)
|
||||
}, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
val maxItemsDatabase = database.historyMaxItems
|
||||
inputText = maxItemsDatabase.toString()
|
||||
setSwitchAction({ isChecked ->
|
||||
inputText = if (!isChecked) {
|
||||
NONE_MAX_HISTORY_ITEMS.toString()
|
||||
} else {
|
||||
DEFAULT_MAX_HISTORY_ITEMS.toString()
|
||||
}
|
||||
showInputText(isChecked)
|
||||
}, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -34,31 +34,29 @@ class DatabaseMaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePrefere
|
||||
setExplanationText(R.string.max_history_size_summary)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.historyMaxSize?.let { maxItemsDatabase ->
|
||||
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
|
||||
.toBetterByteFormat()
|
||||
inputText = dataByte.number.toString()
|
||||
if (dataByte.number >= 0) {
|
||||
setUnitText(dataByte.format.stringId)
|
||||
} else {
|
||||
unitText = null
|
||||
}
|
||||
|
||||
setSwitchAction({ isChecked ->
|
||||
if (!isChecked) {
|
||||
dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
|
||||
inputText = INFINITE_MAX_HISTORY_SIZE.toString()
|
||||
unitText = null
|
||||
} else {
|
||||
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
|
||||
inputText = dataByte.number.toString()
|
||||
setUnitText(dataByte.format.stringId)
|
||||
}
|
||||
showInputText(isChecked)
|
||||
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
val maxItemsDatabase = database.historyMaxSize
|
||||
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
|
||||
.toBetterByteFormat()
|
||||
inputText = dataByte.number.toString()
|
||||
if (dataByte.number >= 0) {
|
||||
setUnitText(dataByte.format.stringId)
|
||||
} else {
|
||||
unitText = null
|
||||
}
|
||||
|
||||
setSwitchAction({ isChecked ->
|
||||
if (!isChecked) {
|
||||
dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
|
||||
inputText = INFINITE_MAX_HISTORY_SIZE.toString()
|
||||
unitText = null
|
||||
} else {
|
||||
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
|
||||
inputText = dataByte.number.toString()
|
||||
setUnitText(dataByte.format.stringId)
|
||||
}
|
||||
showInputText(isChecked)
|
||||
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -34,15 +34,12 @@ class DatabaseMemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreference
|
||||
setExplanationText(R.string.memory_usage_explanation)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
val memoryBytes = database.memoryUsage
|
||||
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
|
||||
.toBetterByteFormat()
|
||||
inputText = dataByte.number.toString()
|
||||
setUnitText(dataByte.format.stringId)
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
val memoryBytes = database.memoryUsage
|
||||
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
|
||||
.toBetterByteFormat()
|
||||
inputText = dataByte.number.toString()
|
||||
setUnitText(dataByte.format.stringId)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
|
||||
class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
inputText = database?.name ?: ""
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
inputText = database.name
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -31,9 +31,8 @@ class DatabaseParallelismPreferenceDialogFragmentCompat : DatabaseSavePreference
|
||||
setExplanationText(R.string.parallelism_explanation)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
inputText = database?.parallelism?.toString() ?: MIN_PARALLELISM.toString()
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
inputText = database.parallelism.toString()
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -48,12 +48,9 @@ class DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
mGroupRecycleBin = database.recycleBin
|
||||
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
mGroupRecycleBin = database.recycleBin
|
||||
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
|
||||
}
|
||||
|
||||
override fun onItemSelected(item: Group) {
|
||||
|
||||
@@ -46,6 +46,8 @@ class DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat : DatabaseSavePre
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {
|
||||
|
||||
@@ -32,9 +32,8 @@ class DatabaseRoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
|
||||
explanationText = getString(R.string.rounds_explanation)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString()
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
inputText = database.numberKeyEncryptionRounds.toString()
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||
|
||||
@@ -22,6 +22,9 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
@@ -32,13 +35,15 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class DatabaseSavePreferenceDialogFragmentCompat
|
||||
: InputPreferenceDialogFragmentCompat(), DatabaseRetrieval {
|
||||
|
||||
private var mDatabaseAutoSaveEnable = true
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabase: ContextualDatabase? = null
|
||||
protected val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
@@ -47,18 +52,32 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
mDatabaseViewModel.database.observe(this) { database ->
|
||||
onDatabaseRetrieved(database)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
onDatabaseRetrieved(mDatabase)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
this.mDatabase = database
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -77,8 +96,10 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
|
||||
// To inherit to save element in database
|
||||
}
|
||||
|
||||
protected fun saveColor(oldColor: Int?,
|
||||
newColor: Int?) {
|
||||
protected fun saveColor(
|
||||
oldColor: Int?,
|
||||
newColor: Int?
|
||||
) {
|
||||
val oldColorString = if (oldColor != null)
|
||||
ChromaUtil.getFormattedColorString(oldColor, false)
|
||||
else
|
||||
@@ -87,77 +108,158 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
|
||||
ChromaUtil.getFormattedColorString(newColor, false)
|
||||
else
|
||||
""
|
||||
mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable)
|
||||
mDatabaseViewModel.saveColor(
|
||||
oldColorString,
|
||||
newColorString,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveCompression(oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm
|
||||
protected fun saveCompression(
|
||||
oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm
|
||||
) {
|
||||
mDatabaseViewModel.saveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
|
||||
mDatabaseViewModel.saveCompression(
|
||||
oldCompression,
|
||||
newCompression,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveDefaultUsername(oldUsername: String,
|
||||
newUsername: String) {
|
||||
mDatabaseViewModel.saveDefaultUsername(oldUsername, newUsername, mDatabaseAutoSaveEnable)
|
||||
protected fun saveDefaultUsername(
|
||||
oldUsername: String,
|
||||
newUsername: String
|
||||
) {
|
||||
mDatabaseViewModel.saveDefaultUsername(
|
||||
oldUsername,
|
||||
newUsername,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveDescription(oldDescription: String,
|
||||
newDescription: String) {
|
||||
mDatabaseViewModel.saveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
|
||||
protected fun saveDescription(
|
||||
oldDescription: String,
|
||||
newDescription: String
|
||||
) {
|
||||
mDatabaseViewModel.saveDescription(
|
||||
oldDescription,
|
||||
newDescription,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveEncryption(oldEncryption: EncryptionAlgorithm,
|
||||
newEncryptionAlgorithm: EncryptionAlgorithm) {
|
||||
mDatabaseViewModel.saveEncryption(oldEncryption, newEncryptionAlgorithm, mDatabaseAutoSaveEnable)
|
||||
protected fun saveEncryption(
|
||||
oldEncryption: EncryptionAlgorithm,
|
||||
newEncryptionAlgorithm: EncryptionAlgorithm
|
||||
) {
|
||||
mDatabaseViewModel.saveEncryption(
|
||||
oldEncryption,
|
||||
newEncryptionAlgorithm,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveKeyDerivation(oldKeyDerivation: KdfEngine,
|
||||
newKeyDerivation: KdfEngine) {
|
||||
mDatabaseViewModel.saveKeyDerivation(oldKeyDerivation, newKeyDerivation, mDatabaseAutoSaveEnable)
|
||||
protected fun saveKeyDerivation(
|
||||
oldKeyDerivation: KdfEngine,
|
||||
newKeyDerivation: KdfEngine
|
||||
) {
|
||||
mDatabaseViewModel.saveKeyDerivation(
|
||||
oldKeyDerivation,
|
||||
newKeyDerivation,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveName(oldName: String,
|
||||
newName: String) {
|
||||
mDatabaseViewModel.saveName(oldName, newName, mDatabaseAutoSaveEnable)
|
||||
protected fun saveName(
|
||||
oldName: String,
|
||||
newName: String
|
||||
) {
|
||||
mDatabaseViewModel.saveName(
|
||||
oldName,
|
||||
newName,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveRecycleBin(oldGroup: Group?,
|
||||
newGroup: Group?) {
|
||||
mDatabaseViewModel.saveRecycleBin(oldGroup, newGroup, mDatabaseAutoSaveEnable)
|
||||
protected fun saveRecycleBin(
|
||||
oldGroup: Group?,
|
||||
newGroup: Group?
|
||||
) {
|
||||
mDatabaseViewModel.saveRecycleBin(
|
||||
oldGroup,
|
||||
newGroup,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun removeUnlinkedData() {
|
||||
mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable)
|
||||
}
|
||||
|
||||
protected fun saveTemplatesGroup(oldGroup: Group?,
|
||||
newGroup: Group?) {
|
||||
mDatabaseViewModel.saveTemplatesGroup(oldGroup, newGroup, mDatabaseAutoSaveEnable)
|
||||
protected fun saveTemplatesGroup(
|
||||
oldGroup: Group?,
|
||||
newGroup: Group?
|
||||
) {
|
||||
mDatabaseViewModel.saveTemplatesGroup(
|
||||
oldGroup,
|
||||
newGroup,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveMaxHistoryItems(oldNumber: Int,
|
||||
newNumber: Int) {
|
||||
mDatabaseViewModel.saveMaxHistoryItems(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
||||
protected fun saveMaxHistoryItems(
|
||||
oldNumber: Int,
|
||||
newNumber: Int
|
||||
) {
|
||||
mDatabaseViewModel.saveMaxHistoryItems(
|
||||
oldNumber,
|
||||
newNumber,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveMaxHistorySize(oldNumber: Long,
|
||||
newNumber: Long) {
|
||||
mDatabaseViewModel.saveMaxHistorySize(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
||||
protected fun saveMaxHistorySize(
|
||||
oldNumber: Long,
|
||||
newNumber: Long
|
||||
) {
|
||||
mDatabaseViewModel.saveMaxHistorySize(
|
||||
oldNumber,
|
||||
newNumber,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveMemoryUsage(oldNumber: Long,
|
||||
newNumber: Long) {
|
||||
mDatabaseViewModel.saveMemoryUsage(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
||||
protected fun saveMemoryUsage(
|
||||
oldNumber: Long,
|
||||
newNumber: Long
|
||||
) {
|
||||
mDatabaseViewModel.saveMemoryUsage(
|
||||
oldNumber,
|
||||
newNumber,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveParallelism(oldNumber: Long,
|
||||
newNumber: Long) {
|
||||
mDatabaseViewModel.saveParallelism(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
||||
protected fun saveParallelism(
|
||||
oldNumber: Long,
|
||||
newNumber: Long
|
||||
) {
|
||||
mDatabaseViewModel.saveParallelism(
|
||||
oldNumber,
|
||||
newNumber,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveIterations(oldNumber: Long,
|
||||
newNumber: Long) {
|
||||
mDatabaseViewModel.saveIterations(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
||||
protected fun saveIterations(
|
||||
oldNumber: Long,
|
||||
newNumber: Long
|
||||
) {
|
||||
mDatabaseViewModel.saveIterations(
|
||||
oldNumber,
|
||||
newNumber,
|
||||
mDatabaseAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -48,12 +48,9 @@ class DatabaseTemplatesGroupPreferenceDialogFragmentCompat
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
mGroupTemplates = database.templatesGroup
|
||||
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
mGroupTemplates = database.templatesGroup
|
||||
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
|
||||
}
|
||||
|
||||
override fun onItemSelected(item: Group) {
|
||||
|
||||
@@ -27,32 +27,27 @@ import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class ProgressTaskDialogFragment : DialogFragment() {
|
||||
|
||||
@StringRes
|
||||
private var title = UNDEFINED
|
||||
@StringRes
|
||||
private var message = UNDEFINED
|
||||
@StringRes
|
||||
private var warning = UNDEFINED
|
||||
private var cancellable: (() -> Unit)? = null
|
||||
|
||||
private var titleView: TextView? = null
|
||||
private var messageView: TextView? = null
|
||||
private var warningView: TextView? = null
|
||||
private var cancelButton: Button? = null
|
||||
private var progressView: ProgressBar? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
private val progressTaskViewModel: ProgressTaskViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
try {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
@@ -71,68 +66,63 @@ open class ProgressTaskDialogFragment : DialogFragment() {
|
||||
cancelButton = root.findViewById(R.id.progress_dialog_cancel)
|
||||
progressView = root.findViewById(R.id.progress_dialog_bar)
|
||||
|
||||
updateTitle(title)
|
||||
updateMessage(message)
|
||||
updateWarning(warning)
|
||||
setCancellable(cancellable)
|
||||
|
||||
isCancelable = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
progressTaskViewModel.progressTaskState.collect { state ->
|
||||
when (state) {
|
||||
is ProgressTaskViewModel.ProgressTaskState.Show -> {
|
||||
val value = state.value
|
||||
updateView(
|
||||
titleView,
|
||||
value.titleId?.let { title ->
|
||||
getString(title)
|
||||
})
|
||||
updateView(
|
||||
messageView,
|
||||
value.messageId?.let { message ->
|
||||
getString(message)
|
||||
})
|
||||
updateView(
|
||||
warningView,
|
||||
value.warningId?.let { warning ->
|
||||
getString(warning)
|
||||
})
|
||||
cancelButton?.apply {
|
||||
isVisible = value.cancelable != null
|
||||
setOnClickListener {
|
||||
value.cancelable?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Nothing here, this fragment is stopped externally
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create progress dialog")
|
||||
Log.e(TAG, "Unable to create progress dialog", e)
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
fun setTitle(@StringRes titleId: Int) {
|
||||
this.title = titleId
|
||||
}
|
||||
|
||||
private fun updateView(textView: TextView?, @StringRes resId: Int) {
|
||||
activity?.lifecycleScope?.launch {
|
||||
if (resId == UNDEFINED) {
|
||||
textView?.visibility = View.GONE
|
||||
} else {
|
||||
textView?.setText(resId)
|
||||
textView?.visibility = View.VISIBLE
|
||||
}
|
||||
private fun updateView(textView: TextView?, value: String?) {
|
||||
if (value == null) {
|
||||
textView?.visibility = View.GONE
|
||||
} else {
|
||||
textView?.text = value
|
||||
textView?.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCancelable() {
|
||||
activity?.lifecycleScope?.launch {
|
||||
cancelButton?.isVisible = cancellable != null
|
||||
cancelButton?.setOnClickListener {
|
||||
cancellable?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle(@StringRes resId: Int?) {
|
||||
this.title = resId ?: UNDEFINED
|
||||
updateView(titleView, title)
|
||||
}
|
||||
|
||||
fun updateMessage(@StringRes resId: Int?) {
|
||||
this.message = resId ?: UNDEFINED
|
||||
updateView(messageView, message)
|
||||
}
|
||||
|
||||
fun updateWarning(@StringRes resId: Int?) {
|
||||
this.warning = resId ?: UNDEFINED
|
||||
updateView(warningView, warning)
|
||||
}
|
||||
|
||||
fun setCancellable(cancellable: (() -> Unit)?) {
|
||||
this.cancellable = cancellable
|
||||
updateCancelable()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ProgressTaskDialogFragment::class.java.simpleName
|
||||
const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment"
|
||||
const val UNDEFINED = -1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.kunzisoft.keepass.tasks
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.kunzisoft.keepass.database.ProgressMessage
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class ProgressTaskViewModel: ViewModel() {
|
||||
|
||||
private val mProgressTaskState = MutableStateFlow<ProgressTaskState>(ProgressTaskState.Hide)
|
||||
val progressTaskState: StateFlow<ProgressTaskState> = mProgressTaskState
|
||||
|
||||
fun show(value: ProgressMessage) {
|
||||
mProgressTaskState.update { currentState ->
|
||||
ProgressTaskState.Show(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
mProgressTaskState.value = ProgressTaskState.Hide
|
||||
}
|
||||
|
||||
sealed class ProgressTaskState {
|
||||
data class Show(val value: ProgressMessage): ProgressTaskState()
|
||||
object Hide: ProgressTaskState()
|
||||
}
|
||||
}
|
||||
@@ -13,15 +13,13 @@ import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||
|
||||
object AppUtil {
|
||||
|
||||
fun randomRequestCode(): Int {
|
||||
return (Math.random() * Integer.MAX_VALUE).toInt()
|
||||
}
|
||||
|
||||
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
|
||||
try {
|
||||
this.applicationContext.packageManager.getPackageInfoCompat(
|
||||
@@ -79,29 +77,6 @@ object AppUtil {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the concrete web domain AKA without sub domain if needed
|
||||
*/
|
||||
fun getConcreteWebDomain(context: Context,
|
||||
webDomain: String?,
|
||||
concreteWebDomain: (String?) -> Unit) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (webDomain != null) {
|
||||
// Warning, web domain can contains IP, don't crop in this case
|
||||
if (PreferencesUtil.searchSubdomains(context)
|
||||
|| Regex(SearchInfo.WEB_IP_REGEX).matches(webDomain)) {
|
||||
concreteWebDomain.invoke(webDomain)
|
||||
} else {
|
||||
val publicSuffixList = PublicSuffixList(context)
|
||||
concreteWebDomain.invoke(publicSuffixList
|
||||
.getPublicSuffixPlusOne(webDomain).await())
|
||||
}
|
||||
} else {
|
||||
concreteWebDomain.invoke(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
|
||||
val packageManager = context.packageManager
|
||||
@@ -123,25 +98,43 @@ object AppUtil {
|
||||
}
|
||||
|
||||
val processedPackageNames = mutableSetOf<String>()
|
||||
|
||||
for (resolveInfo in resolveInfoList) {
|
||||
val packageName = resolveInfo.activityInfo.packageName
|
||||
if (packageName != null && !processedPackageNames.contains(packageName)) {
|
||||
try {
|
||||
val packageInfo = packageManager.getPackageInfo(
|
||||
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)
|
||||
buildAndroidPrivilegedApp(packageManager, packageName)?.let { privilegedApp ->
|
||||
browserList.add(privilegedApp)
|
||||
processedPackageNames.add(packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the Play Service
|
||||
val gServices = "com.google.android.gms"
|
||||
buildAndroidPrivilegedApp(packageManager, gServices)?.let { privilegedApp ->
|
||||
browserList.add(privilegedApp)
|
||||
processedPackageNames.add(gServices)
|
||||
}
|
||||
|
||||
return browserList.distinctBy { it.packageName } // Ensure uniqueness just in case
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
private fun buildAndroidPrivilegedApp(
|
||||
packageManager: PackageManager,
|
||||
packageName: String
|
||||
): AndroidPrivilegedApp? {
|
||||
return try {
|
||||
val packageInfo = packageManager.getPackageInfo(
|
||||
packageName,
|
||||
PackageManager.GET_SIGNING_CERTIFICATES
|
||||
)
|
||||
val signatureFingerprints = packageInfo.signingInfo?.getAllFingerprints()
|
||||
signatureFingerprints?.let {
|
||||
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
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
/**
|
||||
* Class to invoke action in a separate IO thread
|
||||
*/
|
||||
class IOActionTask<T>(
|
||||
private val action: () -> T ,
|
||||
private val afterActionListener: ((T?) -> Unit)? = null) {
|
||||
|
||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
private val action: () -> T,
|
||||
private val onActionComplete: ((T?) -> Unit)? = null,
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
|
||||
private val exceptionHandler: CoroutineExceptionHandler? = null
|
||||
) {
|
||||
fun execute() {
|
||||
mainScope.launch {
|
||||
scope.launch(exceptionHandler ?: EmptyCoroutineContext) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val asyncResult: Deferred<T?> = async {
|
||||
try {
|
||||
action.invoke()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
exceptionHandler?.let {
|
||||
action.invoke()
|
||||
} ?: try {
|
||||
action.invoke()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
afterActionListener?.invoke(asyncResult.await())
|
||||
onActionComplete?.invoke(asyncResult.await())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.view
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ImageView
|
||||
@@ -30,8 +29,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
private var searchTitle: CompoundButton
|
||||
private var searchUsername: CompoundButton
|
||||
private var searchPassword: CompoundButton
|
||||
private var searchApplicationId: CompoundButton
|
||||
private var searchURL: CompoundButton
|
||||
private var searchByURLDomain: Boolean = false
|
||||
private var searchByURLSubDomain: Boolean = false
|
||||
private var searchExpired: CompoundButton
|
||||
private var searchNotes: CompoundButton
|
||||
private var searchOther: CompoundButton
|
||||
@@ -50,8 +51,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
this.searchInTitles = searchTitle.isChecked
|
||||
this.searchInUsernames = searchUsername.isChecked
|
||||
this.searchInPasswords = searchPassword.isChecked
|
||||
this.searchInAppIds = searchApplicationId.isChecked
|
||||
this.searchInUrls = searchURL.isChecked
|
||||
this.searchByDomain = searchByURLDomain
|
||||
this.searchBySubDomain = searchByURLSubDomain
|
||||
this.searchInExpired = searchExpired.isChecked
|
||||
this.searchInNotes = searchNotes.isChecked
|
||||
this.searchInOther = searchOther.isChecked
|
||||
@@ -71,8 +74,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
searchTitle.isChecked = value.searchInTitles
|
||||
searchUsername.isChecked = value.searchInUsernames
|
||||
searchPassword.isChecked = value.searchInPasswords
|
||||
searchApplicationId.isChecked = value.searchInAppIds
|
||||
searchURL.isChecked = value.searchInUrls
|
||||
searchByURLDomain = value.searchByDomain
|
||||
searchByURLSubDomain = value.searchBySubDomain
|
||||
searchExpired.isChecked = value.searchInExpired
|
||||
searchNotes.isChecked = value.searchInNotes
|
||||
searchOther.isChecked = value.searchInOther
|
||||
@@ -87,7 +92,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
|
||||
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
|
||||
// To recalculate height
|
||||
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
|
||||
if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
|
||||
searchAdvanceFiltersContainer?.expand(
|
||||
false,
|
||||
searchAdvanceFiltersContainer?.getFullHeight()
|
||||
@@ -110,6 +115,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
searchTitle = findViewById(R.id.search_chip_title)
|
||||
searchUsername = findViewById(R.id.search_chip_username)
|
||||
searchPassword = findViewById(R.id.search_chip_password)
|
||||
searchApplicationId = findViewById(R.id.search_chip_application_id)
|
||||
searchURL = findViewById(R.id.search_chip_url)
|
||||
searchExpired = findViewById(R.id.search_chip_expires)
|
||||
searchNotes = findViewById(R.id.search_chip_note)
|
||||
@@ -125,7 +131,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
|
||||
// Expand menu with button
|
||||
searchExpandButton.setOnClickListener {
|
||||
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE
|
||||
val isVisible = searchAdvanceFiltersContainer?.visibility == VISIBLE
|
||||
if (isVisible)
|
||||
closeAdvancedFilters()
|
||||
else
|
||||
@@ -156,6 +162,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
searchParameters.searchInPasswords = isChecked
|
||||
mOnParametersChangeListener?.invoke(searchParameters)
|
||||
}
|
||||
searchApplicationId.setOnCheckedChangeListener { _, isChecked ->
|
||||
searchParameters.searchInAppIds = isChecked
|
||||
mOnParametersChangeListener?.invoke(searchParameters)
|
||||
}
|
||||
searchURL.setOnCheckedChangeListener { _, isChecked ->
|
||||
searchParameters.searchInUrls = isChecked
|
||||
mOnParametersChangeListener?.invoke(searchParameters)
|
||||
@@ -200,10 +210,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
|
||||
}
|
||||
|
||||
fun setCurrentGroupText(text: String) {
|
||||
fun setCurrentGroupText(text: String?) {
|
||||
val maxChars = 12
|
||||
searchCurrentGroup.text = when {
|
||||
text.isEmpty() -> context.getString(R.string.current_group)
|
||||
text.isNullOrEmpty() -> context.getString(R.string.current_group)
|
||||
text.length > maxChars -> text.substring(0, maxChars) + "…"
|
||||
else -> text
|
||||
}
|
||||
@@ -213,6 +223,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
searchOther.isVisible = available
|
||||
}
|
||||
|
||||
fun availableApplicationIds(available: Boolean) {
|
||||
searchApplicationId.isVisible = available
|
||||
}
|
||||
|
||||
fun availableTags(available: Boolean) {
|
||||
searchTag.isVisible = available
|
||||
}
|
||||
@@ -243,16 +257,20 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
)
|
||||
}
|
||||
|
||||
fun showSearchExpandButton(show: Boolean) {
|
||||
searchExpandButton.isVisible = show
|
||||
}
|
||||
|
||||
override fun setVisibility(visibility: Int) {
|
||||
when (visibility) {
|
||||
View.VISIBLE -> {
|
||||
searchAdvanceFiltersContainer?.visibility = View.GONE
|
||||
VISIBLE -> {
|
||||
searchAdvanceFiltersContainer?.visibility = GONE
|
||||
searchContainer.showByFading()
|
||||
}
|
||||
else -> {
|
||||
searchContainer.hideByFading()
|
||||
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
|
||||
searchAdvanceFiltersContainer?.visibility = View.INVISIBLE
|
||||
if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
|
||||
searchAdvanceFiltersContainer?.visibility = INVISIBLE
|
||||
searchAdvanceFiltersContainer?.collapse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.animation.AnimatorSet
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
@@ -66,6 +65,7 @@ import com.kunzisoft.keepass.database.exception.LocalizedException
|
||||
import com.kunzisoft.keepass.database.helper.getLocalizedMessage
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import java.util.EnumSet
|
||||
|
||||
|
||||
/**
|
||||
@@ -317,9 +317,7 @@ fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
|
||||
// Only in portrait
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
|
||||
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
|
||||
if (applyToStatusBar) {
|
||||
@@ -335,7 +333,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
|
||||
/**
|
||||
* Apply a margin to a view to fix the window inset
|
||||
*/
|
||||
fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.BOTTOM) {
|
||||
fun View.applyWindowInsets(positions: EnumSet<WindowInsetPosition>) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
|
||||
var consumed = false
|
||||
|
||||
@@ -351,52 +349,78 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
|
||||
}
|
||||
}
|
||||
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
when (position) {
|
||||
WindowInsetPosition.TOP -> {
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()
|
||||
or WindowInsetsCompat.Type.displayCutout()
|
||||
or WindowInsetsCompat.Type.ime())
|
||||
|
||||
val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
|
||||
val wantTopMargins = positions.contains(WindowInsetPosition.TOP_MARGINS)
|
||||
val wantBottomMargins = positions.contains(WindowInsetPosition.BOTTOM_MARGINS)
|
||||
val wantStartMargins = positions.contains(WindowInsetPosition.START_MARGINS)
|
||||
val wantEndMargins = positions.contains(WindowInsetPosition.END_MARGINS)
|
||||
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams
|
||||
&& (wantTopMargins || wantBottomMargins || wantStartMargins || wantEndMargins)) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
if (wantTopMargins) {
|
||||
topMargin = insets.top
|
||||
}
|
||||
if (wantBottomMargins) {
|
||||
bottomMargin = insets.bottom
|
||||
}
|
||||
if (wantStartMargins) {
|
||||
if (isRtl) {
|
||||
rightMargin = insets.right
|
||||
} else {
|
||||
leftMargin = insets.left
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowInsetPosition.LEGIT_TOP -> {
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowInsetPosition.BOTTOM -> {
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowInsetPosition.BOTTOM_IME -> {
|
||||
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = if (imeHeight > 1) 0 else insets.bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowInsetPosition.TOP_BOTTOM_IME -> {
|
||||
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
bottomMargin = if (imeHeight > 1) imeHeight else 0
|
||||
if (wantEndMargins) {
|
||||
if (isRtl) {
|
||||
leftMargin = insets.left
|
||||
} else {
|
||||
rightMargin = insets.right
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val wantTopPadding = positions.contains(WindowInsetPosition.TOP_PADDING)
|
||||
val wantBottomPadding = positions.contains(WindowInsetPosition.BOTTOM_PADDING)
|
||||
val wantStartPadding = positions.contains(WindowInsetPosition.START_PADDING)
|
||||
val wantEndPadding = positions.contains(WindowInsetPosition.END_PADDING)
|
||||
|
||||
if (wantTopPadding || wantBottomPadding || wantStartPadding || wantEndPadding) {
|
||||
val topPadding = if (wantTopPadding) insets.top else 0
|
||||
val bottomPadding = if (wantBottomPadding) insets.bottom else 0
|
||||
var leftPadding = 0
|
||||
var rightPadding = 0
|
||||
|
||||
if (wantStartPadding) {
|
||||
if (isRtl) {
|
||||
rightPadding = insets.right
|
||||
} else {
|
||||
leftPadding = insets.left
|
||||
}
|
||||
}
|
||||
if (wantEndPadding) {
|
||||
if (isRtl) {
|
||||
leftPadding = insets.left
|
||||
} else {
|
||||
rightPadding = insets.right
|
||||
}
|
||||
}
|
||||
|
||||
setPadding(leftPadding, topPadding, rightPadding, bottomPadding)
|
||||
}
|
||||
|
||||
// If any of the children consumed the insets, return an appropriate value
|
||||
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
enum class WindowInsetPosition {
|
||||
TOP, BOTTOM, LEGIT_TOP, BOTTOM_IME, TOP_BOTTOM_IME
|
||||
TOP_MARGINS, BOTTOM_MARGINS, START_MARGINS, END_MARGINS,
|
||||
TOP_PADDING, BOTTOM_PADDING, START_PADDING, END_PADDING,
|
||||
}
|
||||
|
||||
@@ -1,214 +1,500 @@
|
||||
package com.kunzisoft.keepass.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.ProgressMessage
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.UUID
|
||||
|
||||
class DatabaseViewModel: ViewModel() {
|
||||
class DatabaseViewModel(application: Application): AndroidViewModel(application) {
|
||||
|
||||
val database : LiveData<ContextualDatabase?> get() = _database
|
||||
private val _database = MutableLiveData<ContextualDatabase?>()
|
||||
private val mDatabaseState = MutableStateFlow<ContextualDatabase?>(null)
|
||||
val databaseState: StateFlow<ContextualDatabase?> = mDatabaseState
|
||||
|
||||
val actionFinished : LiveData<ActionResult> get() = _actionFinished
|
||||
private val _actionFinished = SingleLiveEvent<ActionResult>()
|
||||
val database: ContextualDatabase?
|
||||
get() = databaseState.value
|
||||
|
||||
val saveDatabase : LiveData<Boolean> get() = _saveDatabase
|
||||
private val _saveDatabase = SingleLiveEvent<Boolean>()
|
||||
private val mActionState = MutableStateFlow<ActionState>(ActionState.Wait)
|
||||
val actionState: StateFlow<ActionState> = mActionState
|
||||
|
||||
val mergeDatabase : LiveData<Boolean> get() = _mergeDatabase
|
||||
private val _mergeDatabase = SingleLiveEvent<Boolean>()
|
||||
private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider(
|
||||
context = application
|
||||
)
|
||||
|
||||
val reloadDatabase : LiveData<Boolean> get() = _reloadDatabase
|
||||
private val _reloadDatabase = SingleLiveEvent<Boolean>()
|
||||
init {
|
||||
mDatabaseTaskProvider.onDatabaseRetrieved = { databaseRetrieved ->
|
||||
val databaseWasReloaded = databaseRetrieved?.wasReloaded == true
|
||||
if (databaseWasReloaded) {
|
||||
mActionState.value = ActionState.OnDatabaseReloaded
|
||||
}
|
||||
if (database == null || database != databaseRetrieved || databaseWasReloaded) {
|
||||
databaseRetrieved?.wasReloaded = false
|
||||
mDatabaseState.value = databaseRetrieved
|
||||
}
|
||||
}
|
||||
mDatabaseTaskProvider.onStartActionRequested = { bundle, actionTask ->
|
||||
mActionState.value = ActionState.OnDatabaseActionRequested(bundle, actionTask)
|
||||
}
|
||||
mDatabaseTaskProvider.databaseInfoListener = object : DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||
override fun onDatabaseInfoChanged(
|
||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnlyDatabase: Boolean
|
||||
) {
|
||||
mActionState.value = ActionState.OnDatabaseInfoChanged(
|
||||
previousDatabaseInfo,
|
||||
newDatabaseInfo,
|
||||
readOnlyDatabase
|
||||
)
|
||||
}
|
||||
}
|
||||
mDatabaseTaskProvider.actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
|
||||
override fun onActionStarted(
|
||||
database: ContextualDatabase,
|
||||
progressMessage: ProgressMessage
|
||||
) {
|
||||
mActionState.value = ActionState.OnDatabaseActionStarted(database, progressMessage)
|
||||
}
|
||||
|
||||
val saveName : LiveData<SuperString> get() = _saveName
|
||||
private val _saveName = SingleLiveEvent<SuperString>()
|
||||
override fun onActionUpdated(
|
||||
database: ContextualDatabase,
|
||||
progressMessage: ProgressMessage
|
||||
) {
|
||||
mActionState.value = ActionState.OnDatabaseActionUpdated(database, progressMessage)
|
||||
}
|
||||
|
||||
val saveDescription : LiveData<SuperString> get() = _saveDescription
|
||||
private val _saveDescription = SingleLiveEvent<SuperString>()
|
||||
override fun onActionStopped(database: ContextualDatabase?) {
|
||||
mActionState.value = ActionState.OnDatabaseActionStopped(database)
|
||||
}
|
||||
|
||||
val saveDefaultUsername : LiveData<SuperString> get() = _saveDefaultUsername
|
||||
private val _saveDefaultUsername = SingleLiveEvent<SuperString>()
|
||||
override fun onActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
mActionState.value = ActionState.OnDatabaseActionFinished(database, actionTask, result)
|
||||
}
|
||||
}
|
||||
|
||||
val saveColor : LiveData<SuperString> get() = _saveColor
|
||||
private val _saveColor = SingleLiveEvent<SuperString>()
|
||||
|
||||
val saveCompression : LiveData<SuperCompression> get() = _saveCompression
|
||||
private val _saveCompression = SingleLiveEvent<SuperCompression>()
|
||||
|
||||
val removeUnlinkData : LiveData<Boolean> get() = _removeUnlinkData
|
||||
private val _removeUnlinkData = SingleLiveEvent<Boolean>()
|
||||
|
||||
val saveRecycleBin : LiveData<SuperGroup> get() = _saveRecycleBin
|
||||
private val _saveRecycleBin = SingleLiveEvent<SuperGroup>()
|
||||
|
||||
val saveTemplatesGroup : LiveData<SuperGroup> get() = _saveTemplatesGroup
|
||||
private val _saveTemplatesGroup = SingleLiveEvent<SuperGroup>()
|
||||
|
||||
val saveMaxHistoryItems : LiveData<SuperInt> get() = _saveMaxHistoryItems
|
||||
private val _saveMaxHistoryItems = SingleLiveEvent<SuperInt>()
|
||||
|
||||
val saveMaxHistorySize : LiveData<SuperLong> get() = _saveMaxHistorySize
|
||||
private val _saveMaxHistorySize = SingleLiveEvent<SuperLong>()
|
||||
|
||||
val saveEncryption : LiveData<SuperEncryption> get() = _saveEncryption
|
||||
private val _saveEncryption = SingleLiveEvent<SuperEncryption>()
|
||||
|
||||
val saveKeyDerivation : LiveData<SuperKeyDerivation> get() = _saveKeyDerivation
|
||||
private val _saveKeyDerivation = SingleLiveEvent<SuperKeyDerivation>()
|
||||
|
||||
val saveIterations : LiveData<SuperLong> get() = _saveIterations
|
||||
private val _saveIterations = SingleLiveEvent<SuperLong>()
|
||||
|
||||
val saveMemoryUsage : LiveData<SuperLong> get() = _saveMemoryUsage
|
||||
private val _saveMemoryUsage = SingleLiveEvent<SuperLong>()
|
||||
|
||||
val saveParallelism : LiveData<SuperLong> get() = _saveParallelism
|
||||
private val _saveParallelism = SingleLiveEvent<SuperLong>()
|
||||
|
||||
|
||||
fun defineDatabase(database: ContextualDatabase?) {
|
||||
this._database.value = database
|
||||
mDatabaseTaskProvider.registerProgressTask()
|
||||
}
|
||||
|
||||
fun onActionFinished(database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result) {
|
||||
this._actionFinished.value = ActionResult(database, actionTask, result)
|
||||
/*
|
||||
* Main database actions
|
||||
*/
|
||||
|
||||
fun loadDatabase(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUuid: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseLoad(
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
fixDuplicateUuid
|
||||
)
|
||||
}
|
||||
|
||||
fun saveDatabase(save: Boolean) {
|
||||
_saveDatabase.value = save
|
||||
fun createDatabase(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseCreate(databaseUri, mainCredential)
|
||||
}
|
||||
|
||||
fun mergeDatabase(save: Boolean) {
|
||||
_mergeDatabase.value = save
|
||||
fun assignMainCredential(
|
||||
databaseUri: Uri?,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
if (databaseUri != null) {
|
||||
mDatabaseTaskProvider.startDatabaseAssignCredential(databaseUri, mainCredential)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDatabase(save: Boolean, saveToUri: Uri? = null) {
|
||||
mDatabaseTaskProvider.startDatabaseSave(save, saveToUri)
|
||||
}
|
||||
|
||||
fun mergeDatabase(
|
||||
save: Boolean,
|
||||
fromDatabaseUri: Uri? = null,
|
||||
mainCredential: MainCredential? = null
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseMerge(save, fromDatabaseUri, mainCredential)
|
||||
}
|
||||
|
||||
fun reloadDatabase(fixDuplicateUuid: Boolean) {
|
||||
_reloadDatabase.value = fixDuplicateUuid
|
||||
mDatabaseTaskProvider.askToStartDatabaseReload(
|
||||
conditionToAsk = database?.dataModifiedSinceLastLoading != false
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseReload(fixDuplicateUuid)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveName(oldValue: String,
|
||||
newValue: String,
|
||||
save: Boolean) {
|
||||
_saveName.value = SuperString(oldValue, newValue, save)
|
||||
fun onDatabaseChangeValidated() {
|
||||
mDatabaseTaskProvider.onDatabaseChangeValidated()
|
||||
}
|
||||
|
||||
fun saveDescription(oldValue: String,
|
||||
newValue: String,
|
||||
save: Boolean) {
|
||||
_saveDescription.value = SuperString(oldValue, newValue, save)
|
||||
/*
|
||||
* Nodes actions
|
||||
*/
|
||||
|
||||
fun createEntry(
|
||||
newEntry: Entry,
|
||||
parent: Group,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseCreateEntry(
|
||||
newEntry,
|
||||
parent,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveDefaultUsername(oldValue: String,
|
||||
newValue: String,
|
||||
save: Boolean) {
|
||||
_saveDefaultUsername.value = SuperString(oldValue, newValue, save)
|
||||
fun updateEntry(
|
||||
oldEntry: Entry,
|
||||
entryToUpdate: Entry,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseUpdateEntry(
|
||||
oldEntry,
|
||||
entryToUpdate,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveColor(oldValue: String,
|
||||
newValue: String,
|
||||
save: Boolean) {
|
||||
_saveColor.value = SuperString(oldValue, newValue, save)
|
||||
fun restoreEntryHistory(
|
||||
mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseRestoreEntryHistory(
|
||||
mainEntryId,
|
||||
entryHistoryPosition,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveCompression(oldValue: CompressionAlgorithm,
|
||||
newValue: CompressionAlgorithm,
|
||||
save: Boolean) {
|
||||
_saveCompression.value = SuperCompression(oldValue, newValue, save)
|
||||
fun deleteEntryHistory(
|
||||
mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseDeleteEntryHistory(
|
||||
mainEntryId,
|
||||
entryHistoryPosition,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun createGroup(
|
||||
newGroup: Group,
|
||||
parent: Group,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseCreateGroup(
|
||||
newGroup,
|
||||
parent,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun updateGroup(
|
||||
oldGroup: Group,
|
||||
groupToUpdate: Group,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseUpdateGroup(
|
||||
oldGroup,
|
||||
groupToUpdate,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun copyNodes(
|
||||
nodesToCopy: List<Node>,
|
||||
newParent: Group,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseCopyNodes(
|
||||
nodesToCopy,
|
||||
newParent,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun moveNodes(
|
||||
nodesToMove: List<Node>,
|
||||
newParent: Group,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseMoveNodes(
|
||||
nodesToMove,
|
||||
newParent,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteNodes(
|
||||
nodes: List<Node>,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseDeleteNodes(
|
||||
nodes,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Attributes
|
||||
*/
|
||||
|
||||
fun buildNewAttachment(): BinaryData? {
|
||||
return database?.buildNewBinaryAttachment()
|
||||
}
|
||||
|
||||
/*
|
||||
* Settings actions
|
||||
*/
|
||||
|
||||
fun saveName(
|
||||
oldValue: String,
|
||||
newValue: String,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveName(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveDescription(
|
||||
oldValue: String,
|
||||
newValue: String,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveDescription(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveDefaultUsername(
|
||||
oldValue: String,
|
||||
newValue: String,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveDefaultUsername(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveColor(
|
||||
oldValue: String,
|
||||
newValue: String,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveColor(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveCompression(
|
||||
oldValue: CompressionAlgorithm,
|
||||
newValue: CompressionAlgorithm,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveCompression(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun removeUnlinkedData(save: Boolean) {
|
||||
_removeUnlinkData.value = save
|
||||
mDatabaseTaskProvider.startDatabaseRemoveUnlinkedData(save)
|
||||
}
|
||||
|
||||
fun saveRecycleBin(oldValue: Group?,
|
||||
newValue: Group?,
|
||||
save: Boolean) {
|
||||
_saveRecycleBin.value = SuperGroup(oldValue, newValue, save)
|
||||
fun saveRecycleBin(
|
||||
oldValue: Group?,
|
||||
newValue: Group?,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveRecycleBin(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveTemplatesGroup(oldValue: Group?,
|
||||
newValue: Group?,
|
||||
save: Boolean) {
|
||||
_saveTemplatesGroup.value = SuperGroup(oldValue, newValue, save)
|
||||
fun saveTemplatesGroup(
|
||||
oldValue: Group?,
|
||||
newValue: Group?,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveTemplatesGroup(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveMaxHistoryItems(oldValue: Int,
|
||||
newValue: Int,
|
||||
save: Boolean) {
|
||||
_saveMaxHistoryItems.value = SuperInt(oldValue, newValue, save)
|
||||
fun saveMaxHistoryItems(
|
||||
oldValue: Int,
|
||||
newValue: Int,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveMaxHistoryItems(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveMaxHistorySize(oldValue: Long,
|
||||
newValue: Long,
|
||||
save: Boolean) {
|
||||
_saveMaxHistorySize.value = SuperLong(oldValue, newValue, save)
|
||||
fun saveMaxHistorySize(
|
||||
oldValue: Long,
|
||||
newValue: Long,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveMaxHistorySize(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun saveEncryption(oldValue: EncryptionAlgorithm,
|
||||
newValue: EncryptionAlgorithm,
|
||||
save: Boolean) {
|
||||
_saveEncryption.value = SuperEncryption(oldValue, newValue, save)
|
||||
fun saveEncryption(
|
||||
oldValue: EncryptionAlgorithm,
|
||||
newValue: EncryptionAlgorithm,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveEncryption(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveKeyDerivation(oldValue: KdfEngine,
|
||||
newValue: KdfEngine,
|
||||
save: Boolean) {
|
||||
_saveKeyDerivation.value = SuperKeyDerivation(oldValue, newValue, save)
|
||||
fun saveKeyDerivation(
|
||||
oldValue: KdfEngine,
|
||||
newValue: KdfEngine,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveKeyDerivation(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveIterations(oldValue: Long,
|
||||
newValue: Long,
|
||||
save: Boolean) {
|
||||
_saveIterations.value = SuperLong(oldValue, newValue, save)
|
||||
fun saveIterations(
|
||||
oldValue: Long,
|
||||
newValue: Long,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveIterations(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveMemoryUsage(oldValue: Long,
|
||||
newValue: Long,
|
||||
save: Boolean) {
|
||||
_saveMemoryUsage.value = SuperLong(oldValue, newValue, save)
|
||||
fun saveMemoryUsage(
|
||||
oldValue: Long,
|
||||
newValue: Long,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveMemoryUsage(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
fun saveParallelism(oldValue: Long,
|
||||
newValue: Long,
|
||||
save: Boolean) {
|
||||
_saveParallelism.value = SuperLong(oldValue, newValue, save)
|
||||
fun saveParallelism(
|
||||
oldValue: Long,
|
||||
newValue: Long,
|
||||
save: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider.startDatabaseSaveParallelism(
|
||||
oldValue,
|
||||
newValue,
|
||||
save
|
||||
)
|
||||
}
|
||||
|
||||
data class ActionResult(val database: ContextualDatabase,
|
||||
val actionTask: String,
|
||||
val result: ActionRunnable.Result)
|
||||
data class SuperString(val oldValue: String,
|
||||
val newValue: String,
|
||||
val save: Boolean)
|
||||
data class SuperInt(val oldValue: Int,
|
||||
val newValue: Int,
|
||||
val save: Boolean)
|
||||
data class SuperLong(val oldValue: Long,
|
||||
val newValue: Long,
|
||||
val save: Boolean)
|
||||
data class SuperMerge(val fixDuplicateUuid: Boolean,
|
||||
val save: Boolean)
|
||||
data class SuperCompression(val oldValue: CompressionAlgorithm,
|
||||
val newValue: CompressionAlgorithm,
|
||||
val save: Boolean)
|
||||
data class SuperEncryption(val oldValue: EncryptionAlgorithm,
|
||||
val newValue: EncryptionAlgorithm,
|
||||
val save: Boolean)
|
||||
data class SuperKeyDerivation(val oldValue: KdfEngine,
|
||||
val newValue: KdfEngine,
|
||||
val save: Boolean)
|
||||
data class SuperGroup(val oldValue: Group?,
|
||||
val newValue: Group?,
|
||||
val save: Boolean)
|
||||
/*
|
||||
* Hardware Key
|
||||
*/
|
||||
|
||||
fun onChallengeResponded(challengeResponse: ByteArray?) {
|
||||
mDatabaseTaskProvider.startChallengeResponded(
|
||||
challengeResponse ?: ByteArray(0)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
mDatabaseTaskProvider.unregisterProgressTask()
|
||||
mDatabaseTaskProvider.destroy()
|
||||
}
|
||||
|
||||
sealed class ActionState {
|
||||
object Wait: 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 androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
@@ -16,10 +17,11 @@ import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.utils.IOActionTask
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
@@ -28,12 +30,18 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
private var mEntryId: NodeId<UUID>? = null
|
||||
private var mParentId: NodeId<*>? = null
|
||||
private var mRegisterInfo: RegisterInfo? = null
|
||||
private var mSearchInfo: SearchInfo? = null
|
||||
private var mParent: Group? = null
|
||||
private var mEntry: Entry? = null
|
||||
private var mIsTemplate: Boolean = false
|
||||
private val mTempAttachments = mutableListOf<EntryAttachmentState>()
|
||||
|
||||
// To show dialog only one time
|
||||
var backPressedAlreadyApproved = false
|
||||
var warningOverwriteDataAlreadyApproved = false
|
||||
|
||||
// Useful to not relaunch a current action
|
||||
private var actionLocked: Boolean = false
|
||||
|
||||
val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
|
||||
private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
|
||||
|
||||
@@ -73,24 +81,28 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
|
||||
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
|
||||
|
||||
fun loadDatabase(database: ContextualDatabase?) {
|
||||
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo)
|
||||
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||
val uiState: StateFlow<UIState> = mUiState
|
||||
|
||||
fun loadTemplateEntry(database: ContextualDatabase?) {
|
||||
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
|
||||
}
|
||||
|
||||
fun loadTemplateEntry(database: ContextualDatabase?,
|
||||
entryId: NodeId<UUID>?,
|
||||
parentId: NodeId<*>?,
|
||||
registerInfo: RegisterInfo?,
|
||||
searchInfo: SearchInfo?) {
|
||||
fun loadTemplateEntry(
|
||||
database: ContextualDatabase?,
|
||||
entryId: NodeId<UUID>?,
|
||||
parentId: NodeId<*>?,
|
||||
registerInfo: RegisterInfo?
|
||||
) {
|
||||
this.mEntryId = entryId
|
||||
this.mParentId = parentId
|
||||
this.mRegisterInfo = registerInfo
|
||||
this.mSearchInfo = searchInfo
|
||||
|
||||
database?.let {
|
||||
mEntryId?.let {
|
||||
IOActionTask(
|
||||
{
|
||||
scope = viewModelScope,
|
||||
action = {
|
||||
// Create an Entry copy to modify from the database entry
|
||||
mEntry = database.getEntryById(it)
|
||||
// Retrieve the parent
|
||||
@@ -105,21 +117,24 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
database,
|
||||
entry,
|
||||
mIsTemplate,
|
||||
registerInfo,
|
||||
searchInfo
|
||||
registerInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
{ templatesEntry ->
|
||||
onActionComplete = { templatesEntry ->
|
||||
mEntryId = null
|
||||
_templatesEntry.value = templatesEntry
|
||||
if (templatesEntry?.overwrittenData == true) {
|
||||
mUiState.value = UIState.ShowOverwriteMessage
|
||||
}
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
mParentId?.let {
|
||||
IOActionTask(
|
||||
{
|
||||
scope = viewModelScope,
|
||||
action = {
|
||||
mParent = database.getGroupById(it)
|
||||
mParent?.let { parentGroup ->
|
||||
mEntry = database.createEntry()?.apply {
|
||||
@@ -145,12 +160,11 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
database,
|
||||
mEntry,
|
||||
mIsTemplate,
|
||||
registerInfo,
|
||||
searchInfo
|
||||
registerInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
{ templatesEntry ->
|
||||
onActionComplete = { templatesEntry ->
|
||||
mParentId = null
|
||||
_templatesEntry.value = templatesEntry
|
||||
}
|
||||
@@ -159,33 +173,37 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeTemplateEntry(database: ContextualDatabase,
|
||||
entry: Entry?,
|
||||
isTemplate: Boolean,
|
||||
registerInfo: RegisterInfo?,
|
||||
searchInfo: SearchInfo?): TemplatesEntry {
|
||||
private fun decodeTemplateEntry(
|
||||
database: ContextualDatabase,
|
||||
entry: Entry?,
|
||||
isTemplate: Boolean,
|
||||
registerInfo: RegisterInfo?
|
||||
): TemplatesEntry {
|
||||
val templates = database.getTemplates(isTemplate)
|
||||
val entryTemplate = entry?.let { database.getTemplate(it) }
|
||||
?: Template.STANDARD
|
||||
var entryInfo: EntryInfo? = null
|
||||
var overwrittenData = false
|
||||
// Decode the entry / load entry info
|
||||
entry?.let {
|
||||
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
|
||||
// Load entry info
|
||||
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
||||
// Retrieve data from registration
|
||||
// TODO only save registration
|
||||
searchInfo?.let { tempSearchInfo ->
|
||||
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
|
||||
}
|
||||
registerInfo?.let { regInfo ->
|
||||
tempEntryInfo.saveRegisterInfo(database, regInfo)
|
||||
overwrittenData = tempEntryInfo.saveRegisterInfo(database, regInfo)
|
||||
}
|
||||
entryInfo = tempEntryInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo)
|
||||
return TemplatesEntry(
|
||||
isTemplate,
|
||||
templates,
|
||||
entryTemplate,
|
||||
entryInfo,
|
||||
overwrittenData
|
||||
)
|
||||
}
|
||||
|
||||
fun changeTemplate(template: Template) {
|
||||
@@ -198,44 +216,52 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
|
||||
}
|
||||
|
||||
fun unlockAction() {
|
||||
actionLocked = false
|
||||
}
|
||||
|
||||
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
|
||||
IOActionTask(
|
||||
{
|
||||
removeTempAttachmentsNotCompleted(entryInfo)
|
||||
entry?.let { oldEntry ->
|
||||
// Create a clone
|
||||
var newEntry = Entry(oldEntry)
|
||||
if (actionLocked.not()) {
|
||||
actionLocked = true
|
||||
IOActionTask(
|
||||
scope = viewModelScope,
|
||||
action = {
|
||||
removeTempAttachmentsNotCompleted(entryInfo)
|
||||
entry?.let { oldEntry ->
|
||||
// Create a clone
|
||||
var newEntry = Entry(oldEntry)
|
||||
|
||||
// Build info
|
||||
newEntry.setEntryInfo(database, entryInfo)
|
||||
// Build info
|
||||
newEntry.setEntryInfo(database, entryInfo)
|
||||
|
||||
// Encode entry properties for template
|
||||
_onTemplateChanged.value?.let { template ->
|
||||
newEntry =
|
||||
database?.encodeEntryWithTemplateConfiguration(newEntry, template)
|
||||
?: newEntry
|
||||
}
|
||||
// Encode entry properties for template
|
||||
_onTemplateChanged.value?.let { template ->
|
||||
newEntry =
|
||||
database?.encodeEntryWithTemplateConfiguration(newEntry, template)
|
||||
?: newEntry
|
||||
}
|
||||
|
||||
// Delete temp attachment if not used
|
||||
mTempAttachments.forEach { tempAttachmentState ->
|
||||
val tempAttachment = tempAttachmentState.attachment
|
||||
database?.attachmentPool?.let { binaryPool ->
|
||||
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
|
||||
database.removeAttachmentIfNotUsed(tempAttachment)
|
||||
// Delete temp attachment if not used
|
||||
mTempAttachments.forEach { tempAttachmentState ->
|
||||
val tempAttachment = tempAttachmentState.attachment
|
||||
database?.attachmentPool?.let { binaryPool ->
|
||||
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
|
||||
database.removeAttachmentIfNotUsed(tempAttachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return entry to save
|
||||
EntrySave(oldEntry, newEntry, parent)
|
||||
// Return entry to save
|
||||
EntrySave(oldEntry, newEntry, parent)
|
||||
}
|
||||
},
|
||||
onActionComplete = { entrySave ->
|
||||
entrySave?.let {
|
||||
_onEntrySaved.value = it
|
||||
}
|
||||
}
|
||||
},
|
||||
{ entrySave ->
|
||||
entrySave?.let {
|
||||
_onEntrySaved.value = it
|
||||
}
|
||||
}
|
||||
).execute()
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) {
|
||||
@@ -322,10 +348,13 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
|
||||
}
|
||||
|
||||
data class TemplatesEntry(val isTemplate: Boolean,
|
||||
val templates: List<Template>,
|
||||
val defaultTemplate: Template,
|
||||
val entryInfo: EntryInfo?)
|
||||
data class TemplatesEntry(
|
||||
val isTemplate: Boolean,
|
||||
val templates: List<Template>,
|
||||
val defaultTemplate: Template,
|
||||
val entryInfo: EntryInfo?,
|
||||
val overwrittenData: Boolean = false
|
||||
)
|
||||
data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?)
|
||||
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
|
||||
data class FieldEdition(val oldField: Field?, val newField: Field?)
|
||||
@@ -333,6 +362,11 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
|
||||
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
|
||||
|
||||
sealed class UIState {
|
||||
object Loading: UIState()
|
||||
object ShowOverwriteMessage: UIState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = EntryEditViewModel::class.java.name
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/activity_entry_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:filterTouchesWhenObscured="true">
|
||||
|
||||
@@ -101,6 +101,13 @@
|
||||
android:checked="false"
|
||||
style="@style/KeepassDXStyle.Chip.Filter"
|
||||
android:text="@string/entry_password"/>
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/search_chip_application_id"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="false"
|
||||
style="@style/KeepassDXStyle.Chip.Filter"
|
||||
android:text="@string/entry_application_id"/>
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/search_chip_url"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -335,7 +335,7 @@
|
||||
<string name="device_unlock_explanation_summary">استخدم إلغاء القفل الجهاز لفتح قاعدة البيانات بسهولة</string>
|
||||
<string name="lock_database_show_button_summary">يعرض زر القَفل في الواجهة</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="clipboard_explanation_summary">انسخ حقول المدخل باستخدام الحافظة</string>
|
||||
<string name="database_opened">قاعدة البيانات مفتوحة</string>
|
||||
@@ -452,7 +452,6 @@
|
||||
<string name="menu_form_filling_settings">ملء النموذج</string>
|
||||
<string name="menu_reload_database">أعد تحميل البيانات</string>
|
||||
<string name="menu_external_icon">أيقونة خارجية</string>
|
||||
<string name="registration_mode">وضع التسجيل</string>
|
||||
<string name="import_app_properties_title">استورد خصائص التطبيق</string>
|
||||
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
|
||||
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string>
|
||||
@@ -643,8 +642,8 @@
|
||||
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
|
||||
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
|
||||
<string name="device_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
|
||||
<string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string>
|
||||
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
|
||||
<string name="menu_appearance_settings_summary">المظاهر والألوان والأيقونات والخطوط والسمات</string>
|
||||
<string name="autofill_explanation_summary">اضبط الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
|
||||
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>
|
||||
<string name="unlock">فتح</string>
|
||||
<string name="menu_app_settings_summary">البحث، القفل، التاريخ، الخصائص</string>
|
||||
@@ -692,4 +691,45 @@
|
||||
<string name="warning_large_keyfile">لا يُنصح بإضافة ملف مفتاحي كبير، فقد يؤدي هذا إلى منع فتح قاعدة البيانات.</string>
|
||||
<string name="hide_templates_title">أخفِ القوالب</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>
|
||||
|
||||
@@ -485,7 +485,6 @@
|
||||
<string name="search_mode">Axtarış modu</string>
|
||||
<string name="save_mode">Yadda saxlama modu</string>
|
||||
<string name="selection_mode">Seçim modu</string>
|
||||
<string name="registration_mode">Qeydiyyat modu</string>
|
||||
<string name="remember_database_locations_title">Məlumat bazalarının yerlərini xatırlayın</string>
|
||||
<string name="remember_database_locations_summary">Məlumat bazalarının harada saxlanıldığını izlə</string>
|
||||
<string name="remember_hardware_key_summary">Aparat-təchizat açarlarının harada istifadə olunduğunu izlə</string>
|
||||
|
||||
@@ -336,7 +336,6 @@
|
||||
<string name="menu_keystore_remove_key">Izbrišite ključ za otključavanje uređaja</string>
|
||||
<string name="subdomain_search_summary">Pretražujte veb domene sa ograničenjima poddomena</string>
|
||||
<string name="export_app_properties_title">Izvezite podešavanja aplikacije</string>
|
||||
<string name="registration_mode">Režim registracije</string>
|
||||
<string name="remember_database_locations_title">Zapamtite lokacije baza podataka</string>
|
||||
<string name="remember_hardware_key_title">Zapamtite hardverske ključeve</string>
|
||||
<string name="remember_hardware_key_summary">Vodi evidenciju o korišćenim hardverskim ključevima</string>
|
||||
|
||||
@@ -296,7 +296,6 @@
|
||||
<string name="search_mode">Рэжым пошуку</string>
|
||||
<string name="save_mode">Рэжым захавання</string>
|
||||
<string name="selection_mode">Рэжым выбару</string>
|
||||
<string name="registration_mode">Рэжым рэгістрацыі</string>
|
||||
<string name="remember_database_locations_title">Запамінаць размяшчэнне баз дадзеных</string>
|
||||
<string name="remember_database_locations_summary">Адсочвае, дзе захоўваюцца базы дадзеных</string>
|
||||
<string name="remember_keyfile_locations_title">Запамінаць размяшчэнне файлаў ключоў</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user