mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
272 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8413d2b31a | ||
|
|
04a03da382 | ||
|
|
e3274657ea | ||
|
|
f3b25cb792 | ||
|
|
d181f886fe | ||
|
|
616d073395 | ||
|
|
d36fc19585 | ||
|
|
95d9e07e2f | ||
|
|
91ebf2ba6f | ||
|
|
9e97042dd1 | ||
|
|
b7c4a99e71 | ||
|
|
48e315453e | ||
|
|
a8a6d14ca3 | ||
|
|
e895dd3430 | ||
|
|
f59859137a | ||
|
|
dee92e9e40 | ||
|
|
6701f4f95e | ||
|
|
e20f769854 | ||
|
|
4f762a9432 | ||
|
|
3c49eb1635 | ||
|
|
bdc6a282e2 | ||
|
|
8392ab2cc4 | ||
|
|
93c7c09f8c | ||
|
|
120116414f | ||
|
|
11794e5819 | ||
|
|
edce3d7bec | ||
|
|
e133e32e7c | ||
|
|
471859e448 | ||
|
|
d8de66eb14 | ||
|
|
cfdc0237d7 | ||
|
|
05fad24eda | ||
|
|
d4818c5567 | ||
|
|
8e8e6a7b93 | ||
|
|
6547f0ffad | ||
|
|
6f172fffa8 | ||
|
|
ed16e06676 | ||
|
|
1874f06f42 | ||
|
|
e9db24429a | ||
|
|
a59f4d45ca | ||
|
|
c7b3e0926c | ||
|
|
f0f5258bc9 | ||
|
|
12c07cf793 | ||
|
|
d2b8c85015 | ||
|
|
b9652291bd | ||
|
|
b0d1f93bfc | ||
|
|
553416c927 | ||
|
|
b83696bc60 | ||
|
|
23ce320d75 | ||
|
|
27d5733dbc | ||
|
|
8d7d01bf88 | ||
|
|
0bc37d2fc2 | ||
|
|
aaa1655af1 | ||
|
|
f1bd4e1bba | ||
|
|
15a28e7c83 | ||
|
|
ab26e561fd | ||
|
|
66968a28a3 | ||
|
|
37d1f91224 | ||
|
|
9e69068d42 | ||
|
|
8e2a9fcd01 | ||
|
|
1753887916 | ||
|
|
21bcffcc87 | ||
|
|
1caed49c75 | ||
|
|
619ea35168 | ||
|
|
877f913e8f | ||
|
|
25c47390c0 | ||
|
|
b3c46348a1 | ||
|
|
6f154194f1 | ||
|
|
c3ab08ce17 | ||
|
|
004fffa992 | ||
|
|
d6bd80c9c0 | ||
|
|
318bcdd011 | ||
|
|
3076f2af68 | ||
|
|
3dd9ef5564 | ||
|
|
367e5fa84e | ||
|
|
97cd61fd13 | ||
|
|
869bf7a345 | ||
|
|
9e15ac242d | ||
|
|
84943e58f1 | ||
|
|
2289bf0a27 | ||
|
|
7fda40c983 | ||
|
|
7eeed8f670 | ||
|
|
4d3f4ed5c2 | ||
|
|
145a4f5c20 | ||
|
|
9afe3d26e9 | ||
|
|
b73a7f1ed8 | ||
|
|
91a2bc3862 | ||
|
|
78a8a840b0 | ||
|
|
f4d54b6ca3 | ||
|
|
bc7a1c332c | ||
|
|
0e75cb9095 | ||
|
|
41b6fb6dcd | ||
|
|
2ca3cbc88f | ||
|
|
d05641a3d6 | ||
|
|
28bf84e05c | ||
|
|
ff51b53660 | ||
|
|
8b8e034b18 | ||
|
|
39927b06e3 | ||
|
|
66db2e7d16 | ||
|
|
a927c33ef1 | ||
|
|
17bc18b881 | ||
|
|
aa643c4a82 | ||
|
|
836fbea676 | ||
|
|
045049243c | ||
|
|
b9813a3494 | ||
|
|
9b42a93ce1 | ||
|
|
8502bceef1 | ||
|
|
663387476f | ||
|
|
daafd83df9 | ||
|
|
f780f2725b | ||
|
|
483aca871a | ||
|
|
352e709c3b | ||
|
|
629057b2c1 | ||
|
|
0e5f53596d | ||
|
|
0d91f07646 | ||
|
|
db882a26ab | ||
|
|
35c8ea22b1 | ||
|
|
7f01619358 | ||
|
|
ee109b4ceb | ||
|
|
7a398e5453 | ||
|
|
23a548f9b4 | ||
|
|
d4655d7034 | ||
|
|
9feb96b541 | ||
|
|
e939278193 | ||
|
|
d4ef1a2617 | ||
|
|
5f8746ced3 | ||
|
|
40a063e94f | ||
|
|
8f5439b958 | ||
|
|
e347f57d8b | ||
|
|
7169b15fd8 | ||
|
|
f9def8c96f | ||
|
|
ef43837af1 | ||
|
|
2efb8e8b8c | ||
|
|
e5bb69ea5f | ||
|
|
0979ca607d | ||
|
|
6ae186b2af | ||
|
|
98fb27f77d | ||
|
|
d68510bbaa | ||
|
|
71fdd2d92d | ||
|
|
3656689ff3 | ||
|
|
7d78406db6 | ||
|
|
ac47748e41 | ||
|
|
80f9b46479 | ||
|
|
999f1bf47a | ||
|
|
9e114eb2b8 | ||
|
|
4177d34b00 | ||
|
|
3ec5c04bf6 | ||
|
|
a877c068b6 | ||
|
|
6a3db90c1e | ||
|
|
a079e0d864 | ||
|
|
719776d66e | ||
|
|
c5af1241e9 | ||
|
|
27e4d7b563 | ||
|
|
450ab34721 | ||
|
|
3e2d4eae2c | ||
|
|
d89b6529ef | ||
|
|
5caf11556a | ||
|
|
78cc6f0f40 | ||
|
|
0007cd4668 | ||
|
|
05195e41de | ||
|
|
66f44ef87d | ||
|
|
a0585d9b11 | ||
|
|
5067946b13 | ||
|
|
f52241d5a8 | ||
|
|
04ccb25fa3 | ||
|
|
5a3f4b60b8 | ||
|
|
4408b2e488 | ||
|
|
9a26acee35 | ||
|
|
6ac377348b | ||
|
|
daeb88d4f4 | ||
|
|
47bf199f52 | ||
|
|
91540b022d | ||
|
|
505a51b6b5 | ||
|
|
28400488aa | ||
|
|
45ae600289 | ||
|
|
8be6874651 | ||
|
|
f694a500e0 | ||
|
|
c415fa01fc | ||
|
|
5225a9459c | ||
|
|
2974b150af | ||
|
|
cf353c8067 | ||
|
|
3aacb5d8b3 | ||
|
|
114fbdbe01 | ||
|
|
f1f83cbec4 | ||
|
|
335c28c2c9 | ||
|
|
daf12cbcce | ||
|
|
bf56eca003 | ||
|
|
a12b7fd58a | ||
|
|
98d004edbf | ||
|
|
67586a98b3 | ||
|
|
c6d8911883 | ||
|
|
12e398ce9b | ||
|
|
5c4a202616 | ||
|
|
adc1ec8444 | ||
|
|
0227d2fcb4 | ||
|
|
95abe3b5ac | ||
|
|
133a902c54 | ||
|
|
aacb03d9ef | ||
|
|
fbeaa1781f | ||
|
|
ca73aad538 | ||
|
|
4a4bfefd17 | ||
|
|
6796b0cd2a | ||
|
|
29e1f824b0 | ||
|
|
51263a2911 | ||
|
|
dd7f857475 | ||
|
|
f0fdd4a537 | ||
|
|
9b847a0561 | ||
|
|
469e76b80a | ||
|
|
9c6994b476 | ||
|
|
52ba487617 | ||
|
|
21c57c9484 | ||
|
|
9b1ea6a07a | ||
|
|
f7d7bb0ea3 | ||
|
|
dd96b9ef53 | ||
|
|
b6b01893ba | ||
|
|
ad531d793d | ||
|
|
a9dd11e24a | ||
|
|
115983830b | ||
|
|
442cece081 | ||
|
|
14e08457b9 | ||
|
|
fabcc08cd5 | ||
|
|
c03188e976 | ||
|
|
34bbd8f439 | ||
|
|
053f57cff5 | ||
|
|
365d2e2844 | ||
|
|
22c0bc0adb | ||
|
|
c4cbf07d78 | ||
|
|
dbb2c10bba | ||
|
|
67a5eef7d6 | ||
|
|
979f651251 | ||
|
|
3b93cbb009 | ||
|
|
30c63bfc4b | ||
|
|
bed40324a1 | ||
|
|
bc035de377 | ||
|
|
4db3cb6936 | ||
|
|
ed4b91f4bd | ||
|
|
24c7151276 | ||
|
|
804a9c07b8 | ||
|
|
528ea56821 | ||
|
|
7ba9c69ff8 | ||
|
|
0fc34da08a | ||
|
|
3fbf8cdbc8 | ||
|
|
e21f20d818 | ||
|
|
9fd9a60ca3 | ||
|
|
78b683d724 | ||
|
|
ad2f5036e1 | ||
|
|
4afbad8faa | ||
|
|
ae8b1c0c29 | ||
|
|
27978c459c | ||
|
|
1dc7f5c666 | ||
|
|
12ac870d3a | ||
|
|
dd1baa0224 | ||
|
|
bb27ef41cc | ||
|
|
2d35ac1df8 | ||
|
|
589ffc0c06 | ||
|
|
1f7f38c7d3 | ||
|
|
83817a2dc0 | ||
|
|
11af9da66f | ||
|
|
af3926acf3 | ||
|
|
ab40c2b3fd | ||
|
|
fd05670dbc | ||
|
|
1ac094bfae | ||
|
|
fdf052cddb | ||
|
|
9a8d50ba6f | ||
|
|
136c97c312 | ||
|
|
bf00b88ef3 | ||
|
|
bafd1ea549 | ||
|
|
982618511b | ||
|
|
a4ad7ca3b1 | ||
|
|
99d71b57a4 | ||
|
|
1b2d8502e0 | ||
|
|
53e4ea9334 | ||
|
|
3ce704155c |
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -24,6 +24,7 @@ A clear and concise description of what you expected to happen.
|
||||
- Created with: [e.g Windows KeePass 2.42]
|
||||
- Version: [e.g. 2]
|
||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
|
||||
- Size: [e.g. 150Mo]
|
||||
- Contains attachment: [e.g. Yes]
|
||||
|
||||
|
||||
23
CHANGELOG
23
CHANGELOG
@@ -1,3 +1,26 @@
|
||||
KeePassDX(3.1.0)
|
||||
* Add breadcrumb
|
||||
* Add path in search results #1148
|
||||
* Add group info dialog #1177
|
||||
* Manage colors #64 #913
|
||||
* Fix UI in Android 8 #509
|
||||
* Upgrade libs and SDK to 31 #833
|
||||
* Fix parser of database v1 #1201
|
||||
* Stop asking WRITE_EXTERNAL_STORAGE permission
|
||||
|
||||
KeePassDX(3.0.4)
|
||||
* Fix autofill inline bugs #1173 #1165
|
||||
* Small UI change
|
||||
|
||||
KeePassDX(3.0.3)
|
||||
* Change default Argon2 parameters #1098
|
||||
* Add & edit custom icon name #976
|
||||
* Fix templates #1128 #1133 #1138
|
||||
* Update Autofill compatibility list #725 #1154
|
||||
* Improve fingerprint usage #1137 #1145
|
||||
* Change backup configuration #1144
|
||||
* Add lock button in database notification
|
||||
|
||||
KeePassDX(3.0.2)
|
||||
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- Material design with **themes**.
|
||||
- **Auto-Fill** and Integration.
|
||||
- Field filling **keyboard**.
|
||||
- Dynamic **templates**
|
||||
- **History** of each entry.
|
||||
- Precise management of **settings**.
|
||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||
@@ -71,7 +72,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@ apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "31.0.0"
|
||||
ndkVersion "21.4.7075529"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 30
|
||||
versionCode = 89
|
||||
versionName = "3.0.2"
|
||||
targetSdkVersion 31
|
||||
versionCode = 92
|
||||
versionName = "3.1.0"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -99,33 +99,33 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
def room_version = "2.2.6"
|
||||
def room_version = "2.4.1"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
implementation 'androidx.media:media:1.4.3'
|
||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation "androidx.core:core-ktx:$android_core_version"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.0'
|
||||
implementation "com.google.android.material:material:$android_material_version"
|
||||
// Database
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
// Autofill
|
||||
implementation "androidx.autofill:autofill:1.1.0"
|
||||
// Time
|
||||
implementation 'joda-time:joda-time:2.10.6'
|
||||
implementation 'joda-time:joda-time:2.10.13'
|
||||
// Color
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
||||
// Education
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
||||
// Apache Commons
|
||||
implementation 'commons-io:commons-io:2.8.0'
|
||||
implementation 'commons-codec:commons-codec:1.15'
|
||||
@@ -136,6 +136,6 @@ dependencies {
|
||||
implementation project(path: ':icon-pack-material')
|
||||
|
||||
// Tests
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||
androidTestImplementation "androidx.test:rules:$android_test_version"
|
||||
}
|
||||
|
||||
@@ -10,15 +10,12 @@
|
||||
android:anyDensity="true" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission
|
||||
android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission
|
||||
android:name="android.permission.VIBRATE"/>
|
||||
<!-- Write permission until Android 10 -->
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<!-- Open apps from links -->
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
@@ -30,12 +27,13 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:name="com.kunzisoft.keepass.app.App"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:fullBackupContent="@xml/old_backup_rules"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||
android:largeHeap="true"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/KeepassDXStyle.Night"
|
||||
tools:targetApi="n">
|
||||
tools:targetApi="s">
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="${googleAndroidBackupAPIKey}" />
|
||||
@@ -44,6 +42,7 @@
|
||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:exported="true"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
||||
<intent-filter>
|
||||
@@ -53,6 +52,7 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||
<intent-filter>
|
||||
@@ -111,6 +111,7 @@
|
||||
<!-- Main Activity -->
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||
android:exported="false"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
<meta-data
|
||||
@@ -154,7 +155,8 @@
|
||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent">
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -173,7 +175,8 @@
|
||||
android:theme="@style/Theme.Transparent" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||
android:label="@string/keyboard_setting_label">
|
||||
android:label="@string/keyboard_setting_label"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
@@ -199,6 +202,7 @@
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||
android:label="@string/autofill_service_name"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||
<meta-data
|
||||
android:name="android.autofill"
|
||||
@@ -210,6 +214,7 @@
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
||||
android:label="@string/keyboard_label"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||
<meta-data android:name="android.view.im"
|
||||
android:resource="@xml/keyboard_method"/>
|
||||
|
||||
@@ -23,28 +23,33 @@ import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
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
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this, true)
|
||||
else null
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
@@ -60,17 +65,37 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||
when (specialMode) {
|
||||
SpecialMode.SELECTION -> {
|
||||
// Build search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchSelection(database, searchInfo)
|
||||
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.getParcelable(KEY_INLINE_SUGGESTION)
|
||||
}
|
||||
// Build search param
|
||||
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||
SearchInfo.getConcreteWebDomain(
|
||||
this,
|
||||
searchInfo.webDomain
|
||||
) { concreteWebDomain ->
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val assistStructure = AutofillHelper
|
||||
.retrieveAutofillComponent(intent)
|
||||
?.assistStructure
|
||||
val newAutofillComponent = if (assistStructure != null) {
|
||||
AutofillComponent(
|
||||
assistStructure,
|
||||
compatInlineSuggestionsRequest
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchSelection(database, newAutofillComponent, searchInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove bundle
|
||||
intent.removeExtra(KEY_SELECTION_BUNDLE)
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
// To register info
|
||||
@@ -91,10 +116,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
|
||||
private fun launchSelection(database: Database?,
|
||||
autofillComponent: AutofillComponent?,
|
||||
searchInfo: SearchInfo) {
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
||||
|
||||
if (autofillComponent == null) {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
@@ -119,6 +142,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillResult(this,
|
||||
openedDatabase,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
@@ -126,6 +150,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
@@ -186,55 +211,47 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
|
||||
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
||||
// Close the database
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION"
|
||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
||||
|
||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||
|
||||
fun getPendingIntentForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
// Doesn't work with Parcelable (don't know why?)
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
searchInfo?.let {
|
||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
|
||||
}
|
||||
// Doesn't work with direct extra Parcelable (don't know why?)
|
||||
// Wrap into a bundle to bypass the problem
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
||||
}
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
})
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
})
|
||||
}
|
||||
|
||||
fun getPendingIntentForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
})
|
||||
}
|
||||
|
||||
fun launchForRegistration(context: Context,
|
||||
|
||||
@@ -32,10 +32,16 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
@@ -57,20 +63,24 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.view.changeControlColor
|
||||
import com.kunzisoft.keepass.view.changeTitleColor
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var appBarLayout: AppBarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
private var entryProgress: ProgressBar? = null
|
||||
private var entryProgress: LinearProgressIndicator? = null
|
||||
private var lockView: View? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
private var loadingView: ProgressBar? = null
|
||||
@@ -84,11 +94,21 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
private var mEntryLoaded = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
private var mAttachmentSelected: Attachment? = null
|
||||
|
||||
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) {
|
||||
// Reload the current id from database
|
||||
mEntryViewModel.loadDatabase(mDatabase)
|
||||
}
|
||||
|
||||
private var mIcon: IconImage? = null
|
||||
private var mIconColor: Int = 0
|
||||
private var mColorAccent: Int = 0
|
||||
private var mControlColor: Int = 0
|
||||
private var mColorPrimary: Int = 0
|
||||
private var mColorBackground: Int = 0
|
||||
private var mBackgroundColor: Int? = null
|
||||
private var mForegroundColor: Int? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -103,6 +123,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
// Get views
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
appBarLayout = findViewById(R.id.app_bar)
|
||||
titleIconView = findViewById(R.id.entry_icon)
|
||||
historyView = findViewById(R.id.history_container)
|
||||
entryProgress = findViewById(R.id.entry_progress)
|
||||
@@ -113,10 +134,19 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
collapsingToolbarLayout?.title = " "
|
||||
toolbar?.title = " "
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
||||
taIconColor.recycle()
|
||||
// Retrieve the textColor to tint the toolbar
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
|
||||
val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary))
|
||||
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||
mColorAccent = taColorAccent.getColor(0, Color.BLACK)
|
||||
mControlColor = taControlColor.getColor(0, Color.BLACK)
|
||||
mColorPrimary = taColorPrimary.getColor(0, Color.BLACK)
|
||||
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
|
||||
taColorAccent.recycle()
|
||||
taControlColor.recycle()
|
||||
taColorPrimary.recycle()
|
||||
taColorBackground.recycle()
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
@@ -133,6 +163,15 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
// Init SAF manager
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||
mAttachmentSelected?.let { attachment ->
|
||||
if (createdFileUri != null) {
|
||||
mAttachmentFileBinderManager
|
||||
?.startDownloadAttachment(createdFileUri, attachment)
|
||||
}
|
||||
mAttachmentSelected = null
|
||||
}
|
||||
}
|
||||
// Init attachment service binder manager
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
|
||||
@@ -152,10 +191,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||
if (entryIsHistory) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim =
|
||||
ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
ColorDrawable(mColorAccent)
|
||||
}
|
||||
|
||||
val entryInfo = entryInfoHistory.entryInfo
|
||||
@@ -170,15 +207,15 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
// Assign title icon
|
||||
mIcon = entryInfo.icon
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
|
||||
}
|
||||
// Assign title text
|
||||
val entryTitle =
|
||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
|
||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
mUrl = entryInfo.url
|
||||
// Assign colors
|
||||
mBackgroundColor = entryInfo.backgroundColor
|
||||
mForegroundColor = entryInfo.foregroundColor
|
||||
|
||||
loadingView?.hideByFading()
|
||||
mEntryLoaded = true
|
||||
@@ -190,9 +227,9 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||
if (otpElement == null)
|
||||
if (otpElement == null) {
|
||||
entryProgress?.visibility = View.GONE
|
||||
when (otpElement?.type) {
|
||||
} else when (otpElement.type) {
|
||||
// Only add token if HOTP
|
||||
OtpType.HOTP -> {
|
||||
entryProgress?.visibility = View.GONE
|
||||
@@ -201,7 +238,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
OtpType.TOTP -> {
|
||||
entryProgress?.apply {
|
||||
max = otpElement.period
|
||||
progress = otpElement.secondsRemaining
|
||||
setProgressCompat(otpElement.secondsRemaining, true)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
@@ -209,9 +246,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
||||
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentSelected
|
||||
}
|
||||
mAttachmentSelected = attachmentSelected
|
||||
mExternalFileHelper?.createDocument(attachmentSelected.name)
|
||||
}
|
||||
|
||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||
@@ -220,7 +256,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
this,
|
||||
database,
|
||||
historySelected.nodeId,
|
||||
historySelected.historyPosition
|
||||
historySelected.historyPosition,
|
||||
mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -238,13 +275,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mEntryViewModel.loadDatabase(database)
|
||||
|
||||
// Assign title icon
|
||||
mIcon?.let { icon ->
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -290,24 +320,27 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
// Reload the current id from database
|
||||
mEntryViewModel.loadDatabase(mDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||
if (createdFileUri != null) {
|
||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||
mAttachmentFileBinderManager
|
||||
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
||||
}
|
||||
private fun applyToolbarColors() {
|
||||
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
|
||||
val backgroundDarker = if (mBackgroundColor != null) {
|
||||
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
||||
} else {
|
||||
mColorBackground
|
||||
}
|
||||
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
|
||||
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
||||
mIcon?.let { icon ->
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(
|
||||
iconView,
|
||||
icon,
|
||||
mForegroundColor ?: mColorAccent
|
||||
)
|
||||
}
|
||||
}
|
||||
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
|
||||
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -346,6 +379,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
applyToolbarColors()
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
@@ -391,7 +425,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
EntryEditActivity.launchToUpdate(
|
||||
this,
|
||||
database,
|
||||
entryId
|
||||
entryId,
|
||||
mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -432,7 +467,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
// Transit data in previous Activity after an update
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||
setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this)
|
||||
setResult(Activity.RESULT_OK, this)
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
@@ -450,15 +485,13 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
database: Database,
|
||||
entryId: NodeId<UUID>) {
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
activity.startActivityForResult(
|
||||
intent,
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
||||
)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -469,16 +502,14 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
fun launch(activity: Activity,
|
||||
database: Database,
|
||||
entryId: NodeId<UUID>,
|
||||
historyPosition: Int) {
|
||||
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)
|
||||
activity.startActivityForResult(
|
||||
intent,
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
||||
)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +33,17 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
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 com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
@@ -69,6 +74,7 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.*
|
||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
@@ -96,6 +102,9 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
private var mTemplate: Template? = null
|
||||
private var mIsTemplate: Boolean = false
|
||||
private var mEntryLoaded: Boolean = false
|
||||
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
|
||||
|
||||
private val mColorPickerViewModel: ColorPickerViewModel by viewModels()
|
||||
|
||||
private var mAllowCustomFields = false
|
||||
private var mAllowOTP = false
|
||||
@@ -106,6 +115,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
// Education
|
||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||
|
||||
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
|
||||
mEntryEditViewModel.selectIcon(icon)
|
||||
}
|
||||
|
||||
// To ask data lost only one time
|
||||
private var backPressedAlreadyApproved = false
|
||||
|
||||
@@ -154,6 +167,21 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
|
||||
// To retrieve attachment
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let { attachmentToUploadUri ->
|
||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||
documentFile.name?.let { fileName ->
|
||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||
.show(supportFragmentManager, "fileTooBigFragment")
|
||||
} else {
|
||||
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
// Verify the education views
|
||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||
@@ -175,11 +203,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
templateSelectorSpinner?.apply {
|
||||
// Build template selector
|
||||
if (templates.isNotEmpty()) {
|
||||
adapter = TemplatesSelectorAdapter(
|
||||
mTemplatesSelectorAdapter = TemplatesSelectorAdapter(
|
||||
this@EntryEditActivity,
|
||||
mIconDrawableFactory,
|
||||
templates
|
||||
)
|
||||
).apply {
|
||||
iconDrawableFactory = mIconDrawableFactory
|
||||
}
|
||||
adapter = mTemplatesSelectorAdapter
|
||||
val selectedTemplate = if (mTemplate != null)
|
||||
mTemplate
|
||||
else
|
||||
@@ -213,7 +243,16 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
|
||||
// View model listeners
|
||||
mEntryEditViewModel.requestIconSelection.observe(this) { iconImage ->
|
||||
IconPickerActivity.launch(this@EntryEditActivity, iconImage)
|
||||
IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.requestColorSelection.observe(this) { color ->
|
||||
ColorPickerDialogFragment.newInstance(color)
|
||||
.show(supportFragmentManager, "ColorPickerFragment")
|
||||
}
|
||||
|
||||
mColorPickerViewModel.colorPicked.observe(this) { color ->
|
||||
mEntryEditViewModel.selectColor(color)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||
@@ -321,6 +360,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
||||
mAllowOTP = database?.allowOTP == true
|
||||
mEntryEditViewModel.loadDatabase(database)
|
||||
mTemplatesSelectorAdapter?.apply {
|
||||
iconDrawableFactory = mIconDrawableFactory
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -472,29 +515,6 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
|
||||
mEntryEditViewModel.selectIcon(icon)
|
||||
}
|
||||
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { attachmentToUploadUri ->
|
||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||
documentFile.name?.let { fileName ->
|
||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||
.show(supportFragmentManager, "fileTooBigFragment")
|
||||
} else {
|
||||
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up OTP (HOTP or TOTP) and add it as extra field
|
||||
*/
|
||||
@@ -585,7 +605,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||
attachmentView,
|
||||
{
|
||||
mExternalFileHelper?.openDocument()
|
||||
addNewAttachment()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
@@ -686,7 +706,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
val intentEntry = Intent()
|
||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||
intentEntry.putExtras(bundle)
|
||||
setResult(ADD_OR_UPDATE_ENTRY_RESULT_CODE, intentEntry)
|
||||
setResult(Activity.RESULT_OK, intentEntry)
|
||||
super.finish()
|
||||
} catch (e: Exception) {
|
||||
// Exception when parcelable can't be done
|
||||
@@ -701,23 +721,46 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
// Keys for current Activity
|
||||
const val KEY_ENTRY = "entry"
|
||||
const val KEY_PARENT = "parent"
|
||||
|
||||
// Keys for callback
|
||||
const val ADD_OR_UPDATE_ENTRY_RESULT_CODE = 31
|
||||
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
|
||||
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?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
)
|
||||
} else {
|
||||
entryAddedOrUpdatedListener.invoke(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun registerForEntryResult(activity: FragmentActivity,
|
||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
||||
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
entryAddedOrUpdatedListener.invoke(
|
||||
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
)
|
||||
} else {
|
||||
entryAddedOrUpdatedListener.invoke(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
||||
*/
|
||||
fun launchToUpdate(activity: Activity,
|
||||
database: Database,
|
||||
entryId: NodeId<UUID>) {
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,12 +770,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
*/
|
||||
fun launchToCreate(activity: Activity,
|
||||
database: Database,
|
||||
groupId: NodeId<*>) {
|
||||
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)
|
||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,8 +839,9 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
* Launch EntryEditActivity to add a new entry in autofill selection
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
database: Database,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
@@ -807,6 +852,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
|
||||
@@ -31,8 +31,10 @@ import android.util.Log
|
||||
import android.view.Menu
|
||||
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.recyclerview.widget.LinearLayoutManager
|
||||
@@ -85,6 +87,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this)
|
||||
else null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -109,6 +116,22 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
// Open database button
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||
mDatabaseFileUri = databaseFileCreatedUri
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
} else {
|
||||
val error = getString(R.string.error_create_database)
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error)
|
||||
}
|
||||
}
|
||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
@@ -256,8 +279,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
* Create a new file by calling the content provider
|
||||
*/
|
||||
private fun createNewFile() {
|
||||
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||
mExternalFileHelper?.createDocument(
|
||||
getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default))
|
||||
}
|
||||
|
||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||
@@ -274,7 +298,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
fileNoFoundAction(exception)
|
||||
},
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher)
|
||||
}
|
||||
|
||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||
@@ -283,7 +308,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,33 +385,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
if (uri != null) {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the created URI from the file manager
|
||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
||||
mDatabaseFileUri = databaseFileCreatedUri
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
} else {
|
||||
val error = getString(R.string.error_create_database)
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
@@ -499,11 +498,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import android.app.TimePickerDialog
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
@@ -33,18 +33,23 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
@@ -58,6 +63,7 @@ import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
@@ -75,6 +81,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
GroupFragment.NodeClickListener,
|
||||
GroupFragment.NodesActionMenuListener,
|
||||
GroupFragment.OnScrollListener,
|
||||
GroupFragment.GroupRefreshedListener,
|
||||
SortDialogFragment.SortSelectionListener {
|
||||
|
||||
// Views
|
||||
@@ -82,18 +89,25 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var lockView: View? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
private var databaseNameContainer: ViewGroup? = null
|
||||
private var databaseColorView: ImageView? = null
|
||||
private var databaseNameView: TextView? = null
|
||||
private var searchContainer: ViewGroup? = null
|
||||
private var searchNumbers: TextView? = null
|
||||
private var searchString: TextView? = null
|
||||
private var toolbarBreadcrumb: Toolbar? = null
|
||||
private var searchTitleView: View? = null
|
||||
private var toolbarAction: ToolbarAction? = null
|
||||
private var iconView: ImageView? = null
|
||||
private var numberChildrenView: TextView? = null
|
||||
private var addNodeButtonView: AddNodeButtonView? = null
|
||||
private var groupNameView: TextView? = null
|
||||
private var groupMetaView: TextView? = null
|
||||
private var breadcrumbListView: RecyclerView? = null
|
||||
private var loadingView: ProgressBar? = null
|
||||
|
||||
private val mGroupViewModel: GroupViewModel by viewModels()
|
||||
private val mGroupEditViewModel: GroupEditViewModel by viewModels()
|
||||
|
||||
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null
|
||||
|
||||
private var mGroupFragment: GroupFragment? = null
|
||||
private var mRecyclingBinEnabled = false
|
||||
private var mRecyclingBinIsCurrentGroup = false
|
||||
@@ -111,7 +125,15 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
|
||||
private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null
|
||||
|
||||
private var mIconColor: Int = 0
|
||||
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
|
||||
// To create tree dialog for icon
|
||||
mGroupEditViewModel.selectIcon(icon)
|
||||
}
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this)
|
||||
else null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -122,13 +144,18 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
// Initialize views
|
||||
rootContainerView = findViewById(R.id.activity_group_container_view)
|
||||
coordinatorLayout = findViewById(R.id.group_coordinator)
|
||||
iconView = findViewById(R.id.group_icon)
|
||||
numberChildrenView = findViewById(R.id.group_numbers)
|
||||
addNodeButtonView = findViewById(R.id.add_node_button)
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
databaseNameContainer = findViewById(R.id.database_name_container)
|
||||
databaseColorView = findViewById(R.id.database_color)
|
||||
databaseNameView = findViewById(R.id.database_name)
|
||||
searchContainer = findViewById(R.id.search_container)
|
||||
searchNumbers = findViewById(R.id.search_numbers)
|
||||
searchString = findViewById(R.id.search_string)
|
||||
toolbarBreadcrumb = findViewById(R.id.toolbar_breadcrumb)
|
||||
searchTitleView = findViewById(R.id.search_title)
|
||||
groupNameView = findViewById(R.id.group_name)
|
||||
groupMetaView = findViewById(R.id.group_meta)
|
||||
breadcrumbListView = findViewById(R.id.breadcrumb_list)
|
||||
toolbarAction = findViewById(R.id.toolbar_action)
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
loadingView = findViewById(R.id.loading)
|
||||
@@ -140,10 +167,42 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
toolbar?.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||
taTextColor.recycle()
|
||||
mBreadcrumbAdapter = BreadcrumbAdapter(this).apply {
|
||||
// Open group on breadcrumb click
|
||||
onItemClickListener = { node, _ ->
|
||||
// If last item & not a virtual root group
|
||||
val currentGroup = mCurrentGroup
|
||||
if (currentGroup != null && node == currentGroup
|
||||
&& (currentGroup != mDatabase?.rootGroup
|
||||
|| mDatabase?.rootGroupIsVirtual == false)
|
||||
) {
|
||||
finishNodeAction()
|
||||
launchDialogToShowGroupInfo(currentGroup)
|
||||
} else {
|
||||
if (mGroupFragment?.nodeActionSelectionMode == true) {
|
||||
finishNodeAction()
|
||||
}
|
||||
mDatabase?.let { database ->
|
||||
onNodeClick(database, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
onLongItemClickListener = { node, position ->
|
||||
val currentGroup = mCurrentGroup
|
||||
if (currentGroup != null && node == currentGroup
|
||||
&& (currentGroup != mDatabase?.rootGroup
|
||||
|| mDatabase?.rootGroupIsVirtual == false)
|
||||
) {
|
||||
finishNodeAction()
|
||||
launchDialogForGroupUpdate(currentGroup)
|
||||
} else {
|
||||
onItemClickListener?.invoke(node, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
breadcrumbListView?.apply {
|
||||
adapter = mBreadcrumbAdapter
|
||||
}
|
||||
|
||||
// Retrieve group if defined at launch
|
||||
manageIntent(intent)
|
||||
@@ -201,21 +260,22 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
|
||||
// Add listeners to the add buttons
|
||||
addNodeButtonView?.setAddGroupClickListener {
|
||||
GroupEditDialogFragment.create(GroupInfo().apply {
|
||||
if (currentGroup.allowAddNoteInGroup) {
|
||||
notes = ""
|
||||
}
|
||||
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||
launchDialogForGroupCreation(currentGroup)
|
||||
}
|
||||
addNodeButtonView?.setAddEntryClickListener {
|
||||
mDatabase?.let { database ->
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
EntryEditActivity.launchToCreate(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
currentGroup.nodeId
|
||||
)
|
||||
mCurrentGroup?.nodeId?.let { currentParentGroupId ->
|
||||
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
||||
EntryEditActivity.launchToCreate(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
currentParentGroupId,
|
||||
resultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// Search not used
|
||||
@@ -243,6 +303,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
EntryEditActivity.launchForAutofillResult(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
currentGroup.nodeId,
|
||||
searchInfo
|
||||
@@ -266,9 +327,6 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
assignGroupViewElements(currentGroup)
|
||||
invalidateOptionsMenu()
|
||||
|
||||
loadingView?.hideByFading()
|
||||
}
|
||||
|
||||
@@ -277,7 +335,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
mGroupEditViewModel.requestIconSelection.observe(this) { iconImage ->
|
||||
IconPickerActivity.launch(this@GroupActivity, iconImage)
|
||||
IconPickerActivity.launch(this@GroupActivity, iconImage, mIconSelectionActivityResultLauncher)
|
||||
}
|
||||
|
||||
mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||
@@ -319,6 +377,29 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
return rootContainerView
|
||||
}
|
||||
|
||||
private fun loadGroup(database: Database?) {
|
||||
when {
|
||||
Intent.ACTION_SEARCH == intent.action -> {
|
||||
finishNodeAction()
|
||||
val searchString =
|
||||
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
||||
mGroupViewModel.loadGroupFromSearch(
|
||||
database,
|
||||
searchString,
|
||||
PreferencesUtil.omitBackup(this)
|
||||
)
|
||||
}
|
||||
mCurrentGroupState == null -> {
|
||||
mRootGroup?.let { rootGroup ->
|
||||
mGroupViewModel.loadGroup(database, rootGroup, 0)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
mGroupViewModel.loadGroup(database, mCurrentGroupState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
@@ -328,17 +409,23 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
&& database?.isRecycleBinEnabled == true
|
||||
|
||||
mRootGroup = database?.rootGroup
|
||||
if (mCurrentGroupState == null) {
|
||||
mRootGroup?.let { rootGroup ->
|
||||
mGroupViewModel.loadGroup(database, rootGroup, 0)
|
||||
}
|
||||
} else {
|
||||
mGroupViewModel.loadGroup(database, mCurrentGroupState)
|
||||
}
|
||||
loadGroup(database)
|
||||
|
||||
// Search suggestion
|
||||
database?.let {
|
||||
databaseNameView?.text = if (it.name.isNotEmpty()) it.name else getString(R.string.database)
|
||||
val customColor = it.customColor
|
||||
if (customColor != null) {
|
||||
databaseColorView?.visibility = View.VISIBLE
|
||||
databaseColorView?.setColorFilter(
|
||||
customColor,
|
||||
PorterDuff.Mode.SRC_IN
|
||||
)
|
||||
} else {
|
||||
databaseColorView?.visibility = View.GONE
|
||||
}
|
||||
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, it)
|
||||
mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory
|
||||
mOnSuggestionListener = object : SearchView.OnSuggestionListener {
|
||||
override fun onSuggestionClick(position: Int): Boolean {
|
||||
mSearchSuggestionAdapter?.let { searchAdapter ->
|
||||
@@ -413,16 +500,27 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
)
|
||||
}
|
||||
}
|
||||
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
|
||||
if (result.isSuccess) {
|
||||
try {
|
||||
if (mCurrentGroup == newNodes[0] as Group)
|
||||
reloadCurrentGroup()
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Unable to perform action after group update",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||
if (!result.isSuccess) {
|
||||
reloadCurrentGroup()
|
||||
}
|
||||
|
||||
finishNodeAction()
|
||||
|
||||
refreshNumberOfChildren(mCurrentGroup)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -447,16 +545,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
|
||||
transformSearchInfoIntent(intent)
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
finishNodeAction()
|
||||
val searchString =
|
||||
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
||||
mGroupViewModel.loadGroupFromSearch(
|
||||
mDatabase,
|
||||
searchString,
|
||||
PreferencesUtil.omitBackup(this)
|
||||
)
|
||||
}
|
||||
loadGroup(mDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,62 +565,44 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onGroupRefreshed() {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
assignGroupViewElements(currentGroup)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignGroupViewElements(group: Group?) {
|
||||
// Assign title
|
||||
if (group != null) {
|
||||
if (groupNameView != null) {
|
||||
val title = group.title
|
||||
groupNameView?.text = if (title.isNotEmpty()) title else getText(R.string.root)
|
||||
groupNameView?.invalidate()
|
||||
}
|
||||
if (groupMetaView != null) {
|
||||
val meta = group.nodeId.toString()
|
||||
groupMetaView?.text = meta
|
||||
if (meta.isNotEmpty()
|
||||
&& !group.isVirtual
|
||||
&& PreferencesUtil.showUUID(this)) {
|
||||
groupMetaView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
groupMetaView?.visibility = View.GONE
|
||||
}
|
||||
groupMetaView?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
if (group?.isVirtual == true) {
|
||||
searchTitleView?.visibility = View.VISIBLE
|
||||
if (toolbar != null) {
|
||||
toolbar?.navigationIcon = null
|
||||
}
|
||||
iconView?.visibility = View.GONE
|
||||
searchContainer?.visibility = View.VISIBLE
|
||||
val title = group.title
|
||||
searchString?.text = if (title.isNotEmpty()) title else ""
|
||||
searchNumbers?.text = group.numberOfChildEntries.toString()
|
||||
databaseNameContainer?.visibility = View.GONE
|
||||
toolbarBreadcrumb?.navigationIcon = null
|
||||
toolbarBreadcrumb?.collapse()
|
||||
} else {
|
||||
searchTitleView?.visibility = View.GONE
|
||||
// Assign the group icon depending of IconPack or custom icon
|
||||
iconView?.visibility = View.VISIBLE
|
||||
group?.let { currentGroup ->
|
||||
iconView?.let { imageView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(
|
||||
imageView,
|
||||
currentGroup.icon,
|
||||
mIconColor
|
||||
)
|
||||
}
|
||||
|
||||
if (toolbar != null) {
|
||||
if (group.containsParent())
|
||||
toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp)
|
||||
else {
|
||||
toolbar?.navigationIcon = null
|
||||
}
|
||||
searchContainer?.visibility = View.GONE
|
||||
databaseNameContainer?.visibility = View.VISIBLE
|
||||
// Refresh breadcrumb
|
||||
if (toolbarBreadcrumb?.isVisible != true) {
|
||||
toolbarBreadcrumb?.expand {
|
||||
setBreadcrumbNode(group)
|
||||
}
|
||||
} else {
|
||||
// Add breadcrumb
|
||||
setBreadcrumbNode(group)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign number of children
|
||||
refreshNumberOfChildren(group)
|
||||
|
||||
// Hide button
|
||||
initAddButton(group)
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun setBreadcrumbNode(group: Group?) {
|
||||
mBreadcrumbAdapter?.apply {
|
||||
setNode(group)
|
||||
breadcrumbListView?.scrollToPosition(itemCount -1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAddButton(group: Group?) {
|
||||
@@ -553,18 +624,6 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshNumberOfChildren(group: Group?) {
|
||||
numberChildrenView?.apply {
|
||||
if (PreferencesUtil.showNumberEntries(context)) {
|
||||
group?.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
|
||||
text = group?.numberOfChildEntries?.toString() ?: ""
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolled(dy: Int) {
|
||||
if (actionNodeMode == null)
|
||||
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
|
||||
@@ -594,11 +653,14 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
val entryVersioned = node as Entry
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
EntryActivity.launch(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
entryVersioned.nodeId
|
||||
)
|
||||
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
||||
EntryActivity.launch(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
entryVersioned.nodeId,
|
||||
resultLauncher
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
// Nothing here, a search is simply performed
|
||||
@@ -788,23 +850,42 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
finishNodeAction()
|
||||
when (node.type) {
|
||||
Type.GROUP -> {
|
||||
mOldGroupToUpdate = node as Group
|
||||
GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo())
|
||||
.show(
|
||||
supportFragmentManager,
|
||||
GroupEditDialogFragment.TAG_CREATE_GROUP
|
||||
)
|
||||
launchDialogForGroupUpdate(node as Group)
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
||||
EntryEditActivity.launchToUpdate(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
(node as Entry).nodeId,
|
||||
resultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
Type.ENTRY -> EntryEditActivity.launchToUpdate(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
(node as Entry).nodeId
|
||||
)
|
||||
}
|
||||
reloadGroupIfSearch()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun launchDialogToShowGroupInfo(group: Group) {
|
||||
GroupDialogFragment.launch(group.getGroupInfo())
|
||||
.show(supportFragmentManager, GroupDialogFragment.TAG_SHOW_GROUP)
|
||||
}
|
||||
|
||||
private fun launchDialogForGroupCreation(group: Group) {
|
||||
GroupEditDialogFragment.create(GroupInfo().apply {
|
||||
if (group.allowAddNoteInGroup) {
|
||||
notes = ""
|
||||
}
|
||||
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||
}
|
||||
|
||||
private fun launchDialogForGroupUpdate(group: Group) {
|
||||
mOldGroupToUpdate = group
|
||||
GroupEditDialogFragment.update(group.getGroupInfo())
|
||||
.show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||
}
|
||||
|
||||
override fun onCopyMenuClick(
|
||||
database: Database,
|
||||
nodes: List<Node>
|
||||
@@ -984,7 +1065,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
|
||||
if (!sortMenuEducationPerformed) {
|
||||
// lockMenuEducationPerformed
|
||||
val lockButtonView = findViewById<View>(R.id.lock_button_icon)
|
||||
val lockButtonView = findViewById<View>(R.id.lock_button)
|
||||
lockButtonView != null
|
||||
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
|
||||
lockButtonView,
|
||||
@@ -1002,7 +1083,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
// TODO change database
|
||||
return true
|
||||
}
|
||||
R.id.menu_search ->
|
||||
@@ -1057,37 +1138,6 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
/*
|
||||
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
|
||||
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
|
||||
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
|
||||
*/
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
var flags = intent.flags
|
||||
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
|
||||
intent.flags = flags
|
||||
}
|
||||
|
||||
super.startActivityForResult(intent, requestCode, options)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// To create tree dialog for icon
|
||||
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
|
||||
mGroupEditViewModel.selectIcon(icon)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
// Directly used the onActivityResult in fragment
|
||||
mGroupFragment?.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun removeSearch() {
|
||||
intent.removeExtra(AUTO_SEARCH_KEY)
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
@@ -1292,8 +1342,9 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
* -------------------------
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
database: Database,
|
||||
activityResultLaunch: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null,
|
||||
autoSearch: Boolean = false) {
|
||||
@@ -1303,6 +1354,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
activityResultLaunch,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
@@ -1335,11 +1387,12 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
* Global Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
fun launch(activity: AppCompatActivity,
|
||||
database: Database,
|
||||
onValidateSpecialMode: () -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit) {
|
||||
onLaunchActivitySpecialMode: () -> Unit,
|
||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
GroupActivity.launch(
|
||||
@@ -1451,6 +1504,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
// Here no search info found, disable auto search
|
||||
GroupActivity.launchForAutofillResult(activity,
|
||||
database,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
|
||||
@@ -27,12 +27,16 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconEditDialogFragment
|
||||
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
@@ -81,6 +85,9 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
addCustomIcon(uri)
|
||||
}
|
||||
|
||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||
|
||||
@@ -139,6 +146,16 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
}
|
||||
uploadButton.isEnabled = true
|
||||
}
|
||||
iconPickerViewModel.customIconUpdated.observe(this) { iconCustomUpdated ->
|
||||
if (iconCustomUpdated.error && !iconCustomUpdated.errorConsumed) {
|
||||
Snackbar.make(coordinatorLayout, iconCustomUpdated.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||
iconCustomUpdated.errorConsumed = true
|
||||
}
|
||||
iconCustomUpdated.iconCustom?.let {
|
||||
mDatabase?.updateCustomIcon(it)
|
||||
}
|
||||
iconPickerViewModel.deselectAllCustomIcons()
|
||||
}
|
||||
}
|
||||
|
||||
override fun viewToInvalidateTimeout(): View? {
|
||||
@@ -197,6 +214,10 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
menu?.findItem(R.id.menu_edit)?.apply {
|
||||
isEnabled = mIconsSelected.size == 1
|
||||
isVisible = isEnabled
|
||||
}
|
||||
menu?.findItem(R.id.menu_delete)?.apply {
|
||||
isEnabled = mCustomIconsSelectionMode
|
||||
isVisible = isEnabled
|
||||
@@ -213,6 +234,9 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
updateCustomIcon(mIconsSelected[0])
|
||||
}
|
||||
R.id.menu_delete -> {
|
||||
mIconsSelected.forEach { iconToRemove ->
|
||||
removeCustomIcon(iconToRemove)
|
||||
@@ -277,6 +301,11 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
|
||||
IconEditDialogFragment.update(iconImageCustom)
|
||||
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
|
||||
}
|
||||
|
||||
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
||||
uploadButton.isEnabled = false
|
||||
iconPickerViewModel.deselectAllCustomIcons()
|
||||
@@ -286,14 +315,6 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
addCustomIcon(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setResult() {
|
||||
setResult(Activity.RESULT_OK, Intent().apply {
|
||||
putExtra(EXTRA_ICON, mIconImage)
|
||||
@@ -308,30 +329,28 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
companion object {
|
||||
|
||||
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
|
||||
|
||||
private const val ICON_SELECTED_REQUEST = 15861
|
||||
private const val EXTRA_ICON = "EXTRA_ICON"
|
||||
|
||||
private const val MAX_ICON_SIZE = 5242880
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) {
|
||||
if (requestCode == ICON_SELECTED_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
||||
fun registerIconSelectionForResult(context: FragmentActivity,
|
||||
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
||||
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(context: Activity,
|
||||
previousIcon: IconImage?) {
|
||||
fun launch(context: FragmentActivity,
|
||||
previousIcon: IconImage?,
|
||||
resultLauncher: ActivityResultLauncher<Intent>) {
|
||||
// Create an instance to return the picker icon
|
||||
context.startActivityForResult(
|
||||
Intent(context,
|
||||
IconPickerActivity::class.java).apply {
|
||||
resultLauncher.launch(
|
||||
Intent(context, IconPickerActivity::class.java).apply {
|
||||
if (previousIcon != null)
|
||||
putExtra(EXTRA_ICON, previousIcon)
|
||||
},
|
||||
ICON_SELECTED_REQUEST)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -35,12 +34,12 @@ import android.view.KeyEvent.KEYCODE_ENTER
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
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.app.ActivityCompat
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -71,11 +70,12 @@ import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
|
||||
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||
class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
@@ -89,7 +89,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||
|
||||
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
|
||||
|
||||
private var mDefaultDatabase: Boolean = false
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
@@ -98,20 +99,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mPermissionAsked = false
|
||||
private var mReadOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
set(value) {
|
||||
infoContainerView?.visibility = if (value) {
|
||||
mReadOnly = true
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this)
|
||||
else null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -133,7 +127,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||
|
||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||
} else {
|
||||
@@ -142,6 +135,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mDatabaseKeyFileUri = uri
|
||||
populateKeyFileTextView(uri)
|
||||
}
|
||||
}
|
||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||
@@ -170,9 +169,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||
}
|
||||
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
||||
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
||||
}
|
||||
|
||||
// Init Biometric elements
|
||||
advancedUnlockFragment = supportFragmentManager
|
||||
@@ -188,21 +184,30 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
|
||||
// Listen password checkbox to init advanced unlock and confirmation button
|
||||
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
|
||||
advancedUnlockFragment?.checkUnlockAvailability()
|
||||
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableOrNotTheConfirmationButton()
|
||||
}
|
||||
|
||||
// Observe if default database
|
||||
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||
mDefaultDatabase = isDefaultDatabase
|
||||
}
|
||||
|
||||
// Observe database file change
|
||||
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||
|
||||
// Force read only if the file does not exists
|
||||
mForceReadOnly = databaseFile?.let {
|
||||
val databaseFileNotExists = databaseFile?.let {
|
||||
!it.databaseFileExists
|
||||
} ?: true
|
||||
infoContainerView?.visibility = if (databaseFileNotExists) {
|
||||
mReadOnly = true
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
mForceReadOnly = databaseFileNotExists
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Post init uri with KeyFile only if needed
|
||||
@@ -232,15 +237,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
}
|
||||
|
||||
// Don't allow auto open prompt if lock become when UI visible
|
||||
mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||
false
|
||||
else
|
||||
mAllowAutoOpenBiometricPrompt
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
}
|
||||
|
||||
checkPermission()
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
|
||||
mDatabase?.let { database ->
|
||||
launchGroupActivityIfLoaded(database)
|
||||
@@ -263,7 +266,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
// Recheck advanced unlock if error
|
||||
advancedUnlockFragment?.initAdvancedUnlockMode()
|
||||
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
|
||||
|
||||
if (result.isSuccess) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
@@ -311,7 +314,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
databaseFileViewModel.removeDefaultDatabase()
|
||||
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,7 +347,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||
}
|
||||
mDatabaseFileUri?.let {
|
||||
databaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +364,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() }
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -435,8 +439,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||
} else {
|
||||
// Init Biometric elements
|
||||
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
||||
mAllowAutoOpenBiometricPrompt)
|
||||
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||
}
|
||||
|
||||
enableOrNotTheConfirmationButton()
|
||||
@@ -496,18 +499,15 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
override fun onPause() {
|
||||
// Reinit locking activity UI variable
|
||||
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||
mAllowAutoOpenBiometricPrompt = true
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
||||
mDatabaseKeyFileUri?.let {
|
||||
outState.putString(KEY_KEYFILE, it.toString())
|
||||
}
|
||||
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
@@ -606,35 +606,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
return true
|
||||
}
|
||||
|
||||
// Check permission
|
||||
private fun checkPermission() {
|
||||
if (Build.VERSION.SDK_INT in 23..28
|
||||
&& !mReadOnly
|
||||
&& !mPermissionAsked) {
|
||||
mPermissionAsked = true
|
||||
// Check self permission to show or not the dialog
|
||||
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
val permissions = arrayOf(writePermission)
|
||||
if (toolbar != null
|
||||
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
when (requestCode) {
|
||||
WRITE_EXTERNAL_STORAGE_REQUEST -> {
|
||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To fix multiple view education
|
||||
private var performedEductionInProgress = false
|
||||
private fun launchEducation(menu: Menu) {
|
||||
@@ -709,45 +680,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mAllowAutoOpenBiometricPrompt = false
|
||||
|
||||
// To get device credential unlock result
|
||||
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// To get entry in result
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
var keyFileResult = false
|
||||
mExternalFileHelper?.let {
|
||||
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
if (uri != null) {
|
||||
mDatabaseKeyFileUri = uri
|
||||
populateKeyFileTextView(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!keyFileResult) {
|
||||
// this block if not a key file response
|
||||
when (resultCode) {
|
||||
DatabaseLockActivity.RESULT_EXIT_LOCK -> {
|
||||
clearCredentialsViews()
|
||||
closeDatabase()
|
||||
}
|
||||
Activity.RESULT_CANCELED -> {
|
||||
clearCredentialsViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = PasswordActivity::class.java.name
|
||||
@@ -761,10 +693,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||
|
||||
private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
@@ -855,15 +783,17 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
@@ -891,12 +821,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
* Global Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
fun launch(activity: AppCompatActivity,
|
||||
databaseUri: Uri,
|
||||
keyFile: Uri?,
|
||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit) {
|
||||
onLaunchActivitySpecialMode: () -> Unit,
|
||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
@@ -926,6 +857,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PasswordActivity.launchForAutofillResult(activity,
|
||||
databaseUri, keyFile,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
@@ -133,6 +132,18 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let { pathUri ->
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
val dialog = builder.create()
|
||||
@@ -208,7 +219,11 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
||||
}
|
||||
|
||||
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
||||
if ((mMasterPassword == null
|
||||
|| mMasterPassword!!.isEmpty())
|
||||
&& (keyFileCheckBox == null
|
||||
|| !keyFileCheckBox!!.isChecked
|
||||
|| keyFileSelectionView?.uri == null)) {
|
||||
error = true
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
}
|
||||
@@ -282,23 +297,6 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { pathUri ->
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.widget.CompoundButton
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.androidclearchroma.view.ChromaColorView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||
|
||||
class ColorPickerDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels()
|
||||
|
||||
private lateinit var enableSwitchView: CompoundButton
|
||||
private lateinit var chromaColorView: ChromaColorView
|
||||
|
||||
private var mDefaultColor = Color.WHITE
|
||||
private var mActivated = false
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null)
|
||||
enableSwitchView = root.findViewById(R.id.switch_element)
|
||||
chromaColorView = root.findViewById(R.id.chroma_color_view)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
|
||||
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
|
||||
}
|
||||
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
|
||||
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
|
||||
}
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(ARG_INITIAL_COLOR)) {
|
||||
mDefaultColor = getInt(ARG_INITIAL_COLOR)
|
||||
}
|
||||
if (containsKey(ARG_ACTIVATED)) {
|
||||
mActivated = getBoolean(ARG_ACTIVATED)
|
||||
}
|
||||
}
|
||||
}
|
||||
enableSwitchView.isChecked = mActivated
|
||||
chromaColorView.currentColor = mDefaultColor
|
||||
|
||||
chromaColorView.setOnColorChangedListener {
|
||||
if (!enableSwitchView.isChecked)
|
||||
enableSwitchView.isChecked = true
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val color: Int? = if (enableSwitchView.isChecked)
|
||||
chromaColorView.currentColor
|
||||
else
|
||||
null
|
||||
mColorPickerViewModel.pickColor(color)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
|
||||
outState.putBoolean(ARG_ACTIVATED, mActivated)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
|
||||
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
|
||||
|
||||
fun newInstance(
|
||||
@ColorInt initialColor: Int?,
|
||||
): ColorPickerDialogFragment {
|
||||
return ColorPickerDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE)
|
||||
putBoolean(ARG_ACTIVATED, initialColor != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||
|
||||
class GroupDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||
private var mGroupInfo = GroupInfo()
|
||||
|
||||
private lateinit var iconView: ImageView
|
||||
private var mIconColor: Int = 0
|
||||
private lateinit var nameTextView: TextView
|
||||
private lateinit var notesTextLabelView: TextView
|
||||
private lateinit var notesTextView: TextView
|
||||
private lateinit var expirationView: DateTimeFieldView
|
||||
private lateinit var creationView: TextView
|
||||
private lateinit var modificationView: TextView
|
||||
private lateinit var uuidContainerView: ViewGroup
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_group, null)
|
||||
iconView = root.findViewById(R.id.group_icon)
|
||||
nameTextView = root.findViewById(R.id.group_name)
|
||||
notesTextLabelView = root.findViewById(R.id.group_note_label)
|
||||
notesTextView = root.findViewById(R.id.group_note)
|
||||
expirationView = root.findViewById(R.id.group_expiration)
|
||||
creationView = root.findViewById(R.id.group_created)
|
||||
modificationView = root.findViewById(R.id.group_modified)
|
||||
uuidContainerView = root.findViewById(R.id.group_UUID_container)
|
||||
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
mIconColor = ta.getColor(0, Color.WHITE)
|
||||
ta.recycle()
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_GROUP_INFO)) {
|
||||
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// populate info in views
|
||||
val title = mGroupInfo.title
|
||||
if (title.isEmpty()) {
|
||||
nameTextView.visibility = View.GONE
|
||||
} else {
|
||||
nameTextView.text = title
|
||||
nameTextView.visibility = View.VISIBLE
|
||||
}
|
||||
val notes = mGroupInfo.notes
|
||||
if (notes == null || notes.isEmpty()) {
|
||||
notesTextLabelView.visibility = View.GONE
|
||||
notesTextView.visibility = View.GONE
|
||||
} else {
|
||||
notesTextView.text = notes
|
||||
notesTextLabelView.visibility = View.VISIBLE
|
||||
notesTextView.visibility = View.VISIBLE
|
||||
}
|
||||
expirationView.activation = mGroupInfo.expires
|
||||
expirationView.dateTime = mGroupInfo.expiryTime
|
||||
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
|
||||
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
|
||||
val uuid = UuidUtil.toHexString(mGroupInfo.id)
|
||||
if (uuid == null || uuid.isEmpty()) {
|
||||
uuidContainerView.visibility = View.GONE
|
||||
} else {
|
||||
uuidReferenceView.text = uuid
|
||||
uuidContainerView.apply {
|
||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok){ _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
data class Error(val isError: Boolean, val messageId: Int?)
|
||||
|
||||
companion object {
|
||||
const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP"
|
||||
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||
|
||||
fun launch(groupInfo: GroupInfo): GroupDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||
val fragment = GroupDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,8 +246,8 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
companion object {
|
||||
|
||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||
private const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||
|
||||
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||
val bundle = Bundle()
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
|
||||
class IconEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private val mIconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||
|
||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||
private lateinit var iconView: ImageView
|
||||
private lateinit var nameTextLayoutView: TextInputLayout
|
||||
private lateinit var nameTextView: TextView
|
||||
|
||||
private var mCustomIcon: IconImageCustom? = null
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
||||
}
|
||||
mCustomIcon?.let { customIcon ->
|
||||
populateViewsWithCustomIcon(customIcon)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_edit, null)
|
||||
iconView = root.findViewById(R.id.icon_edit_image)
|
||||
nameTextLayoutView = root.findViewById(R.id.icon_edit_name_container)
|
||||
nameTextView = root.findViewById(R.id.icon_edit_name)
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||
mCustomIcon = savedInstanceState.getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||
mCustomIcon = getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
retrieveIconInfoFromViews()
|
||||
mCustomIcon?.let { customIcon ->
|
||||
mIconPickerViewModel.updateCustomIcon(
|
||||
IconPickerViewModel.IconCustomState(customIcon, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// Do nothing
|
||||
mIconPickerViewModel.updateCustomIcon(
|
||||
IconPickerViewModel.IconCustomState(null, false)
|
||||
)
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun populateViewsWithCustomIcon(customIcon: IconImageCustom) {
|
||||
mPopulateIconMethod?.invoke(iconView, customIcon.getIconImageToDraw())
|
||||
nameTextView.text = customIcon.name
|
||||
}
|
||||
|
||||
private fun retrieveIconInfoFromViews() {
|
||||
mCustomIcon?.name = nameTextView.text.toString()
|
||||
mCustomIcon?.lastModificationTime = DateInstant()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
retrieveIconInfoFromViews()
|
||||
outState.putParcelable(KEY_CUSTOM_ICON_ID, mCustomIcon)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG_UPDATE_ICON = "TAG_UPDATE_ICON"
|
||||
const val KEY_CUSTOM_ICON_ID = "KEY_CUSTOM_ICON_ID"
|
||||
|
||||
fun update(customIcon: IconImageCustom): IconEditDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(KEY_CUSTOM_ICON_ID, IconImageCustom(customIcon))
|
||||
val fragment = IconEditDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +309,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
s?.toString()?.let { userString ->
|
||||
try {
|
||||
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
|
||||
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
||||
otpSecretContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||
|
||||
@@ -99,6 +99,12 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
setOnIconClickListener {
|
||||
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
||||
}
|
||||
setOnBackgroundColorClickListener {
|
||||
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
|
||||
}
|
||||
setOnForegroundColorClickListener {
|
||||
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
|
||||
}
|
||||
setOnCustomEditionActionClickListener { field ->
|
||||
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||
}
|
||||
@@ -147,6 +153,14 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
templateView.setIcon(iconImage)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onBackgroundColorSelected.observe(this) { color ->
|
||||
templateView.setBackgroundColor(color)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onForegroundColorSelected.observe(this) { color ->
|
||||
templateView.setForegroundColor(color)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
|
||||
templateView.setPasswordField(passwordField)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ class EntryFragment: DatabaseFragment() {
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
|
||||
private lateinit var uuidContainerView: View
|
||||
private lateinit var uuidView: TextView
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
private var mClipboardHelper: ClipboardHelper? = null
|
||||
@@ -88,7 +87,6 @@ class EntryFragment: DatabaseFragment() {
|
||||
uuidContainerView.apply {
|
||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||
}
|
||||
uuidView = view.findViewById(R.id.entry_UUID)
|
||||
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||
|
||||
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
||||
@@ -200,7 +198,6 @@ class EntryFragment: DatabaseFragment() {
|
||||
}
|
||||
|
||||
private fun assignUUID(uuid: UUID?) {
|
||||
uuidView.text = uuid?.toString()
|
||||
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
@@ -34,12 +33,11 @@ import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
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.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
@@ -50,10 +48,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
private var nodeClickListener: NodeClickListener? = null
|
||||
private var onScrollListener: OnScrollListener? = null
|
||||
private var groupRefreshed: GroupRefreshedListener? = null
|
||||
|
||||
private var mNodesRecyclerView: RecyclerView? = null
|
||||
private var mLayoutManager: LinearLayoutManager? = null
|
||||
private var mAdapter: NodeAdapter? = null
|
||||
private var mAdapter: NodesAdapter? = null
|
||||
|
||||
private val mGroupViewModel: GroupViewModel by activityViewModels()
|
||||
|
||||
@@ -74,6 +73,19 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
private var mRecycleBinEnable: Boolean = false
|
||||
private var mRecycleBin: Group? = null
|
||||
|
||||
var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
|
||||
entryId?.let {
|
||||
// Simply refresh the list
|
||||
rebuildList()
|
||||
// Scroll to the new entry
|
||||
mDatabase?.getEntryById(it)?.let { entry ->
|
||||
mAdapter?.indexOf(entry)?.let { position ->
|
||||
mNodesRecyclerView?.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
||||
}
|
||||
|
||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
@@ -89,12 +101,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
// TODO Change to ViewModel
|
||||
try {
|
||||
nodeClickListener = context as NodeClickListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
||||
+ " must implement " + NodesAdapter.NodeClickCallback::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -102,14 +116,24 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
} catch (e: ClassCastException) {
|
||||
onScrollListener = null
|
||||
// Context menu can be omit
|
||||
Log.w(TAG, context.toString()
|
||||
Log.w(
|
||||
TAG, context.toString()
|
||||
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
groupRefreshed = context as GroupRefreshedListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GroupRefreshedListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
nodeClickListener = null
|
||||
onScrollListener = null
|
||||
groupRefreshed = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
@@ -125,8 +149,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
contextThemed?.let { context ->
|
||||
database?.let { database ->
|
||||
mAdapter = NodeAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
||||
mAdapter = NodesAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(database: Database, node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
@@ -182,7 +206,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
// To apply theme
|
||||
return inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_group, container, false)
|
||||
.inflate(R.layout.fragment_nodes, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -247,6 +271,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
} else {
|
||||
notFoundView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
groupRefreshed?.onGroupRefreshed()
|
||||
}
|
||||
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||
@@ -279,15 +305,17 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
val sortDialogFragment: SortDialogFragment =
|
||||
if (mRecycleBinEnable) {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
PreferencesUtil.getRecycleBinBottomSort(context))
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
)
|
||||
} else {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context))
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context)
|
||||
)
|
||||
}
|
||||
|
||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||
@@ -399,27 +427,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) {
|
||||
data?.getParcelableExtra<NodeId<UUID>>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let {
|
||||
// Simply refresh the list
|
||||
rebuildList()
|
||||
// Scroll to the new entry
|
||||
mDatabase?.getEntryById(it)?.let { entry ->
|
||||
mAdapter?.indexOf(entry)?.let { position ->
|
||||
mNodesRecyclerView?.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
@@ -455,6 +462,10 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
fun onScrolled(dy: Int)
|
||||
}
|
||||
|
||||
interface GroupRefreshedListener {
|
||||
fun onGroupRefreshed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = GroupFragment::class.java.name
|
||||
}
|
||||
|
||||
@@ -55,8 +55,10 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||
iconCustomAdded?.iconCustom?.let { icon ->
|
||||
iconPickerAdapter.addIcon(icon)
|
||||
iconCustomAdded.iconCustom = null
|
||||
try {
|
||||
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
||||
} catch (ignore: Exception) {}
|
||||
}
|
||||
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
||||
}
|
||||
}
|
||||
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
|
||||
@@ -67,6 +69,14 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||
}
|
||||
}
|
||||
}
|
||||
iconPickerViewModel.customIconUpdated.observe(viewLifecycleOwner) { iconCustomUpdated ->
|
||||
if (!iconCustomUpdated.error) {
|
||||
iconCustomUpdated?.iconCustom?.let { icon ->
|
||||
iconPickerAdapter.updateIcon(icon)
|
||||
iconCustomUpdated.iconCustom = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIconClickListener(icon: IconImageCustom) {
|
||||
|
||||
@@ -20,14 +20,16 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||
@@ -38,6 +40,10 @@ class ExternalFileHelper {
|
||||
private var activity: FragmentActivity? = null
|
||||
private var fragment: Fragment? = null
|
||||
|
||||
private var getContentResultLauncher: ActivityResultLauncher<String>? = null
|
||||
private var openDocumentResultLauncher: ActivityResultLauncher<Array<String>>? = null
|
||||
private var createDocumentResultLauncher: ActivityResultLauncher<String>? = null
|
||||
|
||||
constructor(context: FragmentActivity) {
|
||||
this.activity = context
|
||||
this.fragment = null
|
||||
@@ -48,94 +54,81 @@ class ExternalFileHelper {
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
|
||||
|
||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||
result?.let { uri ->
|
||||
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
||||
onFileSelected?.invoke(uri)
|
||||
}
|
||||
}
|
||||
|
||||
getContentResultLauncher = if (fragment != null) {
|
||||
fragment?.registerForActivityResult(
|
||||
GetContent(),
|
||||
resultCallback
|
||||
)
|
||||
} else {
|
||||
activity?.registerForActivityResult(
|
||||
GetContent(),
|
||||
resultCallback
|
||||
)
|
||||
}
|
||||
|
||||
openDocumentResultLauncher = if (fragment != null) {
|
||||
fragment?.registerForActivityResult(
|
||||
OpenDocument(),
|
||||
resultCallback
|
||||
)
|
||||
} else {
|
||||
activity?.registerForActivityResult(
|
||||
OpenDocument(),
|
||||
resultCallback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun buildCreateDocument(typeString: String = "application/octet-stream",
|
||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||
|
||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||
onFileCreated.invoke(result)
|
||||
}
|
||||
|
||||
createDocumentResultLauncher = if (fragment != null) {
|
||||
fragment?.registerForActivityResult(
|
||||
CreateDocument(typeString),
|
||||
resultCallback
|
||||
)
|
||||
} else {
|
||||
activity?.registerForActivityResult(
|
||||
CreateDocument(typeString),
|
||||
resultCallback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openDocument(getContent: Boolean = false,
|
||||
typeString: String = "*/*") {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
try {
|
||||
if (getContent) {
|
||||
openActivityWithActionGetContent(typeString)
|
||||
} else {
|
||||
openActivityWithActionOpenDocument(typeString)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open document", e)
|
||||
showFileManagerDialogFragment()
|
||||
try {
|
||||
if (getContent) {
|
||||
getContentResultLauncher?.launch(typeString)
|
||||
} else {
|
||||
openDocumentResultLauncher?.launch(arrayOf(typeString))
|
||||
}
|
||||
} else {
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open document", e)
|
||||
showFileManagerDialogFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun openActivityWithActionOpenDocument(typeString: String) {
|
||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
}
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
fun createDocument(titleString: String) {
|
||||
try {
|
||||
createDocumentResultLauncher?.launch(titleString)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create document", e)
|
||||
showFileManagerDialogFragment()
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
else
|
||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun openActivityWithActionGetContent(typeString: String) {
|
||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
}
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
else
|
||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* To use in onActivityResultCallback in Fragment or Activity
|
||||
* @param onFileSelected Callback retrieve from data
|
||||
* @return true if requestCode was captured, false elsewhere
|
||||
*/
|
||||
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
|
||||
|
||||
when (requestCode) {
|
||||
FILE_BROWSE -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
val filename = data?.dataString
|
||||
var keyUri: Uri? = null
|
||||
if (filename != null) {
|
||||
keyUri = UriUtil.parse(filename)
|
||||
}
|
||||
onFileSelected?.invoke(keyUri)
|
||||
}
|
||||
return true
|
||||
}
|
||||
GET_CONTENT, OPEN_DOC -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (data != null) {
|
||||
val uri = data.data
|
||||
if (uri != null) {
|
||||
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
||||
onFileSelected?.invoke(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,62 +148,50 @@ class ExternalFileHelper {
|
||||
}
|
||||
}
|
||||
|
||||
fun createDocument(titleString: String,
|
||||
typeString: String = "application/octet-stream"): Int? {
|
||||
val idCode = getUnusedCreateFileRequestCode()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
putExtra(Intent.EXTRA_TITLE, titleString)
|
||||
class OpenDocument : ActivityResultContracts.OpenDocument() {
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun createIntent(context: Context, input: Array<out String>): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intent, idCode)
|
||||
else
|
||||
activity?.startActivityForResult(intent, idCode)
|
||||
return idCode
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create document", e)
|
||||
showFileManagerDialogFragment()
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
} else {
|
||||
showFileManagerDialogFragment()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* To use in onActivityResultCallback in Fragment or Activity
|
||||
* @param onFileCreated Callback retrieve from data
|
||||
* @return true if requestCode was captured, false elsewhere
|
||||
*/
|
||||
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||
// Retrieve the created URI from the file manager
|
||||
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
|
||||
onFileCreated.invoke(data?.data)
|
||||
fileRequestCodes.remove(requestCode)
|
||||
class GetContent : ActivityResultContracts.GetContent() {
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
}
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "OpenFileHelper"
|
||||
|
||||
private const val GET_CONTENT = 25745
|
||||
private const val OPEN_DOC = 25845
|
||||
private const val FILE_BROWSE = 25645
|
||||
|
||||
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
|
||||
private var fileRequestCodes = ArrayList<Int>()
|
||||
|
||||
private fun getUnusedCreateFileRequestCode(): Int {
|
||||
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
|
||||
fileRequestCodes.add(newCreateFileRequestCode)
|
||||
return newCreateFileRequestCode
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
||||
typeString: String = "application/octet-stream"): Boolean {
|
||||
@@ -231,7 +212,7 @@ class ExternalFileHelper {
|
||||
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||
externalFileHelper?.let { fileHelper ->
|
||||
setOnClickListener {
|
||||
fileHelper.openDocument()
|
||||
fileHelper.openDocument(false)
|
||||
}
|
||||
setOnLongClickListener {
|
||||
fileHelper.openDocument(true)
|
||||
|
||||
@@ -100,7 +100,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
||||
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveColor.observe(this) {
|
||||
@@ -180,8 +180,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
closeDatabase(database)
|
||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||
// Add onActivityForResult response
|
||||
setResult(RESULT_EXIT_LOCK)
|
||||
mExitLock = true
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
}
|
||||
@@ -353,14 +352,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == RESULT_EXIT_LOCK) {
|
||||
mExitLock = true
|
||||
lockAndExit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkRegister() {
|
||||
// If in ave or registration mode, don't allow read only
|
||||
if ((mSpecialMode == SpecialMode.SAVE
|
||||
@@ -440,8 +431,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
|
||||
const val TAG = "LockingActivity"
|
||||
|
||||
const val RESULT_EXIT_LOCK = 1450
|
||||
|
||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||
|
||||
|
||||
@@ -39,7 +39,11 @@ object Stylish {
|
||||
*/
|
||||
fun load(context: Context) {
|
||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||
themeString = PreferencesUtil.getStyle(context)
|
||||
try {
|
||||
themeString = PreferencesUtil.getStyle(context)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Stylish", "Unable to get preference style", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
||||
|
||||
@@ -28,7 +28,7 @@ import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED
|
||||
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
|
||||
|
||||
/**
|
||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||
@@ -89,8 +89,8 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
super.onResume()
|
||||
|
||||
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|
||||
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) {
|
||||
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
|
||||
|| DATABASE_PREFERENCE_CHANGED) {
|
||||
DATABASE_PREFERENCE_CHANGED = false
|
||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||
recreateActivity()
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
abstract class StylishFragment : Fragment() {
|
||||
|
||||
@@ -42,7 +42,6 @@ abstract class StylishFragment : Fragment() {
|
||||
contextThemed = ContextThemeWrapper(context, themeId)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
// To fix status bar color
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
@@ -58,6 +57,7 @@ abstract class StylishFragment : Fragment() {
|
||||
try {
|
||||
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
||||
if (taWindowStatusLight?.getBoolean(0, false) == true) {
|
||||
@Suppress("DEPRECATION")
|
||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||
}
|
||||
taWindowStatusLight?.recycle()
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
|
||||
class BreadcrumbAdapter(val context: Context)
|
||||
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var iconDrawableFactory: IconDrawableFactory? = null
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
private var mNodeBreadcrumb: MutableList<Node?> = mutableListOf()
|
||||
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||
|
||||
private var mShowNumberEntries = false
|
||||
private var mShowUUID = false
|
||||
private var mIconColor: Int = 0
|
||||
|
||||
init {
|
||||
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
mShowUUID = PreferencesUtil.showUUID(context)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setNode(node: Node?) {
|
||||
mNodeBreadcrumb.clear()
|
||||
node?.let {
|
||||
var currentNode = it
|
||||
mNodeBreadcrumb.add(0, currentNode)
|
||||
while (currentNode.containsParent()) {
|
||||
currentNode.parent?.let { parent ->
|
||||
currentNode = parent
|
||||
mNodeBreadcrumb.add(0, currentNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (position) {
|
||||
mNodeBreadcrumb.size - 1 -> 0
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
|
||||
return BreadcrumbGroupViewHolder(inflater.inflate(
|
||||
when (viewType) {
|
||||
0 -> R.layout.item_group
|
||||
else -> R.layout.item_breadcrumb
|
||||
}, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BreadcrumbGroupViewHolder, position: Int) {
|
||||
val node = mNodeBreadcrumb[position]
|
||||
|
||||
holder.groupNameView.apply {
|
||||
text = node?.title ?: ""
|
||||
strikeOut(node?.isCurrentlyExpires ?: false)
|
||||
}
|
||||
|
||||
holder.itemView.apply {
|
||||
setOnClickListener {
|
||||
node?.let {
|
||||
onItemClickListener?.invoke(it, position)
|
||||
}
|
||||
}
|
||||
setOnLongClickListener {
|
||||
node?.let {
|
||||
onLongItemClickListener?.invoke(it, position)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
if (node?.type == Type.GROUP) {
|
||||
(node as Group).let { group ->
|
||||
|
||||
holder.groupIconView?.let { imageView ->
|
||||
iconDrawableFactory?.assignDatabaseIcon(
|
||||
imageView,
|
||||
group.icon,
|
||||
mIconColor
|
||||
)
|
||||
}
|
||||
|
||||
holder.groupNumbersView?.apply {
|
||||
if (mShowNumberEntries) {
|
||||
group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
|
||||
text = group.numberOfChildEntries.toString()
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
holder.groupMetaView?.apply {
|
||||
val meta = group.nodeId.toVisualString()
|
||||
visibility = if (meta != null
|
||||
&& !group.isVirtual
|
||||
&& mShowUUID
|
||||
) {
|
||||
text = meta
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return mNodeBreadcrumb.size
|
||||
}
|
||||
|
||||
inner class BreadcrumbGroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var groupIconView: ImageView? = itemView.findViewById(R.id.group_icon)
|
||||
var groupNumbersView: TextView? = itemView.findViewById(R.id.group_numbers)
|
||||
var groupNameView: TextView = itemView.findViewById(R.id.group_name)
|
||||
var groupMetaView: TextView? = itemView.findViewById(R.id.group_meta)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
@@ -55,9 +56,9 @@ import java.util.*
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
class NodeAdapter (private val context: Context,
|
||||
private val database: Database)
|
||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||
class NodesAdapter (private val context: Context,
|
||||
private val database: Database)
|
||||
: RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
|
||||
|
||||
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||
@@ -79,6 +80,8 @@ class NodeAdapter (private val context: Context,
|
||||
private var mShowOTP: Boolean = false
|
||||
private var mShowUUID: Boolean = false
|
||||
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
||||
private var mOldVirtualGroup = false
|
||||
private var mVirtualGroup = false
|
||||
|
||||
private var mActionNodesList = LinkedList<Node>()
|
||||
private var mNodeClickCallback: NodeClickCallback? = null
|
||||
@@ -87,9 +90,15 @@ class NodeAdapter (private val context: Context,
|
||||
@ColorInt
|
||||
private val mContentSelectionColor: Int
|
||||
@ColorInt
|
||||
private val mIconGroupColor: Int
|
||||
private val mTextColorPrimary: Int
|
||||
@ColorInt
|
||||
private val mIconEntryColor: Int
|
||||
private val mTextColor: Int
|
||||
@ColorInt
|
||||
private val mTextColorSecondary: Int
|
||||
@ColorInt
|
||||
private val mColorAccentLight: Int
|
||||
@ColorInt
|
||||
private val mTextColorInverse: Int
|
||||
|
||||
/**
|
||||
* Determine if the adapter contains or not any element
|
||||
@@ -110,12 +119,24 @@ class NodeAdapter (private val context: Context,
|
||||
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
||||
// Retrieve the color to tint the icon
|
||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||
taTextColorPrimary.recycle()
|
||||
// In two times to fix bug compilation
|
||||
// To get text color
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
||||
this.mTextColor = taTextColor.getColor(0, Color.BLACK)
|
||||
taTextColor.recycle()
|
||||
// To get text color secondary
|
||||
val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
|
||||
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
|
||||
taTextColorSecondary.recycle()
|
||||
// To get background color for selection
|
||||
val taSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
|
||||
this.mColorAccentLight = taSelectionColor.getColor(0, Color.GRAY)
|
||||
taSelectionColor.recycle()
|
||||
// To get text color for selection
|
||||
val taSelectionTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
this.mTextColorInverse = taSelectionTextColor.getColor(0, Color.WHITE)
|
||||
taSelectionTextColor.recycle()
|
||||
}
|
||||
|
||||
private fun assignPreferences() {
|
||||
@@ -145,6 +166,8 @@ class NodeAdapter (private val context: Context,
|
||||
* Rebuild the list by clear and build children from the group
|
||||
*/
|
||||
fun rebuildList(group: Group) {
|
||||
mOldVirtualGroup = mVirtualGroup
|
||||
mVirtualGroup = group.isVirtual
|
||||
assignPreferences()
|
||||
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
||||
}
|
||||
@@ -155,14 +178,19 @@ class NodeAdapter (private val context: Context,
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||
if (mOldVirtualGroup != mVirtualGroup)
|
||||
return false
|
||||
var typeContentTheSame = true
|
||||
if (oldItem is Entry && newItem is Entry) {
|
||||
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
|
||||
&& oldItem.username == newItem.username
|
||||
&& oldItem.backgroundColor == newItem.backgroundColor
|
||||
&& oldItem.foregroundColor == newItem.foregroundColor
|
||||
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
||||
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
||||
} else if (oldItem is Group && newItem is Group) {
|
||||
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
||||
&& oldItem.notes == newItem.notes
|
||||
}
|
||||
return typeContentTheSame
|
||||
&& oldItem.nodeId == newItem.nodeId
|
||||
@@ -327,8 +355,8 @@ class NodeAdapter (private val context: Context,
|
||||
val iconColor = if (holder.container.isSelected)
|
||||
mContentSelectionColor
|
||||
else when (subNode.type) {
|
||||
Type.GROUP -> mIconGroupColor
|
||||
Type.ENTRY -> mIconEntryColor
|
||||
Type.GROUP -> mTextColorPrimary
|
||||
Type.ENTRY -> mTextColor
|
||||
}
|
||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||
holder.icon.apply {
|
||||
@@ -348,14 +376,24 @@ class NodeAdapter (private val context: Context,
|
||||
}
|
||||
// Add meta text to show UUID
|
||||
holder.meta.apply {
|
||||
if (mShowUUID) {
|
||||
text = subNode.nodeId.toString()
|
||||
val nodeId = subNode.nodeId?.toVisualString()
|
||||
if (mShowUUID && nodeId != null) {
|
||||
text = nodeId
|
||||
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
// Add path to virtual group
|
||||
if (mVirtualGroup) {
|
||||
holder.path?.apply {
|
||||
text = subNode.getPathString()
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
holder.path?.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Specific elements for entry
|
||||
if (subNode.type == Type.ENTRY) {
|
||||
@@ -398,6 +436,50 @@ class NodeAdapter (private val context: Context,
|
||||
holder.attachmentIcon?.visibility =
|
||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||
|
||||
// Assign colors
|
||||
val backgroundColor = entry.backgroundColor
|
||||
if (!holder.container.isSelected) {
|
||||
if (backgroundColor != null) {
|
||||
holder.container.setBackgroundColor(backgroundColor)
|
||||
} else {
|
||||
holder.container.setBackgroundColor(Color.TRANSPARENT)
|
||||
}
|
||||
} else {
|
||||
holder.container.setBackgroundColor(mColorAccentLight)
|
||||
}
|
||||
val foregroundColor = entry.foregroundColor
|
||||
if (!holder.container.isSelected) {
|
||||
if (foregroundColor != null) {
|
||||
holder.text.setTextColor(foregroundColor)
|
||||
holder.subText?.setTextColor(foregroundColor)
|
||||
holder.otpToken?.setTextColor(foregroundColor)
|
||||
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
||||
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
||||
holder.meta.setTextColor(foregroundColor)
|
||||
holder.icon.apply {
|
||||
database.iconDrawableFactory.assignDatabaseIcon(
|
||||
this,
|
||||
subNode.icon,
|
||||
foregroundColor
|
||||
)
|
||||
}
|
||||
} else {
|
||||
holder.text.setTextColor(mTextColor)
|
||||
holder.subText?.setTextColor(mTextColorSecondary)
|
||||
holder.otpToken?.setTextColor(mTextColorSecondary)
|
||||
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
||||
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
||||
holder.meta.setTextColor(mTextColor)
|
||||
}
|
||||
} else {
|
||||
holder.text.setTextColor(mTextColorInverse)
|
||||
holder.subText?.setTextColor(mTextColorInverse)
|
||||
holder.otpToken?.setTextColor(mTextColorInverse)
|
||||
holder.otpProgress?.setIndicatorColor(mTextColorInverse)
|
||||
holder.attachmentIcon?.setColorFilter(mTextColorInverse)
|
||||
holder.meta.setTextColor(mTextColorInverse)
|
||||
}
|
||||
|
||||
database.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
@@ -430,15 +512,16 @@ class NodeAdapter (private val context: Context,
|
||||
OtpType.HOTP -> {
|
||||
holder?.otpProgress?.apply {
|
||||
max = 100
|
||||
progress = 100
|
||||
setProgressCompat(100, true)
|
||||
}
|
||||
}
|
||||
OtpType.TOTP -> {
|
||||
holder?.otpProgress?.apply {
|
||||
max = otpElement.period
|
||||
progress = otpElement.secondsRemaining
|
||||
setProgressCompat(otpElement.secondsRemaining, true)
|
||||
}
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
holder?.otpToken?.apply {
|
||||
text = otpElement?.token
|
||||
@@ -497,8 +580,9 @@ class NodeAdapter (private val context: Context,
|
||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||
var subText: TextView? = itemView.findViewById(R.id.node_subtext)
|
||||
var meta: TextView = itemView.findViewById(R.id.node_meta)
|
||||
var path: TextView? = itemView.findViewById(R.id.node_path)
|
||||
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
|
||||
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)
|
||||
var otpProgress: CircularProgressIndicator? = itemView.findViewById(R.id.node_otp_progress)
|
||||
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
|
||||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||
@@ -506,6 +590,6 @@ class NodeAdapter (private val context: Context,
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = NodeAdapter::class.java.name
|
||||
private val TAG = NodesAdapter::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,9 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
|
||||
|
||||
class TemplatesSelectorAdapter(private val context: Context,
|
||||
private val iconDrawableFactory: IconDrawableFactory?,
|
||||
private var templates: List<Template>): BaseAdapter() {
|
||||
|
||||
var iconDrawableFactory: IconDrawableFactory? = null
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var mIconColor = Color.BLACK
|
||||
|
||||
|
||||
@@ -4,4 +4,4 @@ import android.app.assist.AssistStructure
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
|
||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
||||
val inlineSuggestionsRequest: InlineSuggestionsRequest?)
|
||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
||||
@@ -25,7 +25,6 @@ import android.app.PendingIntent
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
@@ -35,11 +34,13 @@ import android.service.autofill.InlinePresentation
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillManager
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -49,21 +50,19 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import kotlin.collections.ArrayList
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
object AutofillHelper {
|
||||
|
||||
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
||||
|
||||
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||
const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||
|
||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||
@@ -112,7 +111,7 @@ object AutofillHelper {
|
||||
database: Database,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?): Dataset? {
|
||||
additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
|
||||
val title = makeEntryTitle(entryInfo)
|
||||
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
||||
val builder = Dataset.Builder(views)
|
||||
@@ -201,11 +200,7 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlinePresentation?.let {
|
||||
builder.setInlinePresentation(it)
|
||||
}
|
||||
}
|
||||
additionalBuild?.invoke(builder)
|
||||
|
||||
return try {
|
||||
builder.build()
|
||||
@@ -236,40 +231,51 @@ object AutofillHelper {
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForEntry(context: Context,
|
||||
database: Database,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo): InlinePresentation? {
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||
|
||||
if (positionItem <= maxSuggestion - 1
|
||||
&& inlinePresentationSpecs.size > positionItem) {
|
||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||
if (positionItem <= maxSuggestion - 1
|
||||
&& inlinePresentationSpecs.size > positionItem
|
||||
) {
|
||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||
|
||||
// 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))
|
||||
return null
|
||||
// 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))
|
||||
return null
|
||||
|
||||
// Build the content for IME UI
|
||||
val pendingIntent = PendingIntent.getActivity(context,
|
||||
// Build the content for IME UI
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, AutofillSettingsActivity::class.java),
|
||||
0)
|
||||
return InlinePresentation(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
return InlinePresentation(
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
||||
setTitle(entryInfo.title)
|
||||
setSubtitle(entryInfo.username)
|
||||
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
setStartIcon(
|
||||
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
||||
setEndIcon(icon.apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
}
|
||||
}.build().slice, inlinePresentationSpec, false)
|
||||
}.build().slice, inlinePresentationSpec, false
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -299,7 +305,7 @@ object AutofillHelper {
|
||||
database: Database,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
// Add Header
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -320,7 +326,7 @@ object AutofillHelper {
|
||||
// Add inline suggestion for new IME and dataset
|
||||
var numberInlineSuggestions = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||
@@ -332,14 +338,19 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
entriesInfo.forEachIndexed { _, entry ->
|
||||
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry)
|
||||
}
|
||||
if (numberInlineSuggestions > 0
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& compatInlineSuggestionsRequest != null) {
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
|
||||
buildInlinePresentationForEntry(context, database,
|
||||
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
|
||||
)?.let { inlinePresentation ->
|
||||
builder.setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
null
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
|
||||
}
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
|
||||
}
|
||||
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
@@ -351,14 +362,14 @@ object AutofillHelper {
|
||||
}
|
||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||
searchInfo, inlineSuggestionsRequest)
|
||||
searchInfo, compatInlineSuggestionsRequest)
|
||||
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
autofillIds.forEach { id ->
|
||||
val builder = Dataset.Builder(manualSelectionView)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
||||
inlinePresentation?.let {
|
||||
@@ -403,11 +414,11 @@ object AutofillHelper {
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
if (inlineSuggestionsRequest != null) {
|
||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<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, inlineSuggestionsRequest)
|
||||
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
|
||||
} else {
|
||||
buildResponse(activity, database, entriesInfo, result, null)
|
||||
}
|
||||
@@ -427,37 +438,44 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
|
||||
fun buildActivityResultLauncher(activity: AppCompatActivity,
|
||||
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
|
||||
return activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
// Utility method to loop and close each activity with return data
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
activity.setResult(it.resultCode, it.data)
|
||||
}
|
||||
if (it.resultCode == Activity.RESULT_CANCELED) {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
activity.finish()
|
||||
|
||||
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
|
||||
// Close the database
|
||||
activity.sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to start an activity with an Autofill for result
|
||||
*/
|
||||
fun startActivityForAutofillResult(activity: Activity,
|
||||
fun startActivityForAutofillResult(activity: AppCompatActivity,
|
||||
intent: Intent,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
||||
autofillComponent.inlineSuggestionsRequest?.let {
|
||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
}
|
||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to loop and close each activity with return data
|
||||
*/
|
||||
fun onActivityResultSetResultAndFinish(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
activity.setResult(resultCode, data)
|
||||
}
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
activity.finish()
|
||||
}
|
||||
activityResultLauncher?.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.autofill
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.service.autofill.FillRequest
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
/**
|
||||
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class CompatInlineSuggestionsRequest : Parcelable {
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.R)
|
||||
var inlineSuggestionsRequest: InlineSuggestionsRequest? = null
|
||||
private set
|
||||
|
||||
constructor(fillRequest: FillRequest) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
this.inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
|
||||
} else {
|
||||
this.inlineSuggestionsRequest = null
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
constructor(inlineSuggestionsRequest: InlineSuggestionsRequest?) {
|
||||
this.inlineSuggestionsRequest = inlineSuggestionsRequest
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
this.inlineSuggestionsRequest =
|
||||
parcel.readParcelable(FillRequest::class.java.classLoader)
|
||||
}
|
||||
else {
|
||||
this.inlineSuggestionsRequest = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
parcel.writeParcelable(inlineSuggestionsRequest, flags)
|
||||
}
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<CompatInlineSuggestionsRequest> {
|
||||
override fun createFromParcel(parcel: Parcel): CompatInlineSuggestionsRequest {
|
||||
return CompatInlineSuggestionsRequest(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<CompatInlineSuggestionsRequest?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -109,7 +109,7 @@ class KeeAutofillService : AutofillService() {
|
||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
request.inlineSuggestionsRequest
|
||||
CompatInlineSuggestionsRequest(request)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -127,7 +127,7 @@ class KeeAutofillService : AutofillService() {
|
||||
private fun launchSelection(database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
@@ -155,7 +155,7 @@ class KeeAutofillService : AutofillService() {
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
@@ -249,7 +249,7 @@ class KeeAutofillService : AutofillService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
inlineSuggestionsRequest?.let {
|
||||
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||
&& inlinePresentationSpecs.size > 0) {
|
||||
@@ -262,9 +262,13 @@ class KeeAutofillService : AutofillService() {
|
||||
inlinePresentation = InlinePresentation(
|
||||
InlineSuggestionUi.newContentBuilder(
|
||||
PendingIntent.getActivity(this,
|
||||
0,
|
||||
Intent(this, AutofillSettingsActivity::class.java),
|
||||
0)
|
||||
0,
|
||||
Intent(this, AutofillSettingsActivity::class.java),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
})
|
||||
).apply {
|
||||
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
||||
setTitle(getString(R.string.autofill_sign_in_prompt))
|
||||
@@ -277,8 +281,9 @@ class KeeAutofillService : AutofillService() {
|
||||
}
|
||||
// Build response
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||
} else {
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||
}
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||
callback.onSuccess(responseBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,12 +272,12 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
|
||||
val autofillId = node.autofillId
|
||||
val nodHtml = node.htmlInfo
|
||||
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
|
||||
when (nodHtml?.tag?.lowercase(Locale.ENGLISH)) {
|
||||
"input" -> {
|
||||
nodHtml.attributes?.forEach { pairAttribute ->
|
||||
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
|
||||
when (pairAttribute.first.lowercase(Locale.ENGLISH)) {
|
||||
"type" -> {
|
||||
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
||||
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
|
||||
"tel", "email" -> {
|
||||
result?.usernameId = autofillId
|
||||
result?.usernameValue = node.autofillValue
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -27,9 +28,11 @@ import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -39,6 +42,7 @@ import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -59,9 +63,12 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
/**
|
||||
* Manage setting to auto open biometric prompt
|
||||
*/
|
||||
private var mAutoOpenPrompt: Boolean = false
|
||||
private var mAutoOpenPrompt: Boolean
|
||||
get() {
|
||||
return field && mAutoOpenPromptEnabled
|
||||
return mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt && mAutoOpenPromptEnabled
|
||||
}
|
||||
set(value) {
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = value
|
||||
}
|
||||
|
||||
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
||||
@@ -72,6 +79,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
|
||||
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||
|
||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
|
||||
|
||||
// Only to fix multiple fingerprint menu #332
|
||||
private var mAllowAdvancedUnlockMenu = false
|
||||
private var mAddBiometricMenuInProgress = false
|
||||
@@ -79,6 +88,15 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
// Only keep connection when we request a device credential activity
|
||||
private var keepConnection = false
|
||||
|
||||
private var mDeviceCredentialResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
// To wait resume
|
||||
if (keepConnection) {
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = result.resultCode == Activity.RESULT_OK
|
||||
}
|
||||
keepConnection = false
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
@@ -97,10 +115,21 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
retainInstance = true
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
|
||||
|
||||
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
|
||||
initAdvancedUnlockMode()
|
||||
}
|
||||
|
||||
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
|
||||
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
|
||||
onDatabaseLoaded(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@@ -114,17 +143,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
return rootView
|
||||
}
|
||||
|
||||
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
|
||||
private var activityResult: ActivityResult? = null
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
// To wait resume
|
||||
if (keepConnection) {
|
||||
activityResult = ActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
keepConnection = false
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
context?.let {
|
||||
@@ -154,32 +172,38 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
|
||||
private fun onDatabaseLoaded(databaseUri: Uri?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// To get device credential unlock result, only if same database uri
|
||||
if (databaseUri != null
|
||||
&& mAdvancedUnlockEnabled) {
|
||||
activityResult?.let {
|
||||
val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
|
||||
deviceCredentialAuthSucceeded?.let {
|
||||
if (databaseUri == databaseFileUri) {
|
||||
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
|
||||
if (deviceCredentialAuthSucceeded == true) {
|
||||
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||
} else {
|
||||
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
|
||||
}
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
} ?: run {
|
||||
this.mAutoOpenPrompt = autoOpenPrompt
|
||||
connect(databaseUri)
|
||||
if (databaseUri != databaseFileUri) {
|
||||
connect(databaseUri)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
activityResult = null
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlock availability and change the current mode depending of device's state
|
||||
*/
|
||||
fun checkUnlockAvailability() {
|
||||
private fun checkUnlockAvailability() {
|
||||
context?.let { context ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
allowOpenBiometricPrompt = true
|
||||
@@ -317,7 +341,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
if (cryptoPrompt.isDeviceCredentialOperation)
|
||||
keepConnection = true
|
||||
try {
|
||||
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
|
||||
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
|
||||
mDeviceCredentialResultLauncher)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open advanced unlock prompt", e)
|
||||
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
||||
@@ -369,8 +394,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun initAdvancedUnlockMode() {
|
||||
private fun initAdvancedUnlockMode() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mAllowAdvancedUnlockMenu = false
|
||||
try {
|
||||
@@ -444,6 +468,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun deleteEncryptedDatabaseKey() {
|
||||
mAllowAdvancedUnlockMenu = false
|
||||
advancedUnlockManager?.closeBiometricPrompt()
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||
@@ -516,6 +541,11 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||
|
||||
@@ -19,15 +19,18 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.*
|
||||
@@ -35,6 +38,7 @@ import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.security.KeyStore
|
||||
import java.security.UnrecoverableKeyException
|
||||
@@ -136,18 +140,24 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
// and the constrains (purposes) in the constructor of the Builder
|
||||
keyGenerator?.init(
|
||||
KeyGenParameterSpec.Builder(
|
||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
.apply {
|
||||
// Require the user to authenticate with a fingerprint to authorize every use
|
||||
// of the key, don't use it for device credential because it's the user authentication
|
||||
.apply {
|
||||
if (biometricUnlockEnable) {
|
||||
setUserAuthenticationRequired(true)
|
||||
}
|
||||
if (biometricUnlockEnable) {
|
||||
setUserAuthenticationRequired(true)
|
||||
}
|
||||
.build())
|
||||
// To store in the security chip
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
&& retrieveContext().packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
|
||||
setIsStrongBoxBacked(true)
|
||||
}
|
||||
}
|
||||
.build())
|
||||
keyGenerator?.generateKey()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -164,8 +174,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
return null
|
||||
}
|
||||
|
||||
fun initEncryptData(actionIfCypherInit
|
||||
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||
fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
||||
initEncryptData(actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
@@ -185,10 +199,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||
advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
|
||||
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
||||
initEncryptData(actionIfCypherInit, false)
|
||||
} else {
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
@@ -214,8 +233,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
||||
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||
fun initDecryptData(ivSpecValue: String,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
private fun initDecryptData(ivSpecValue: String,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean = true) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
@@ -239,10 +264,20 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
||||
deleteKeystoreKey()
|
||||
if (firstLaunch) {
|
||||
deleteKeystoreKey()
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
||||
} else {
|
||||
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
||||
}
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
||||
} else {
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
@@ -278,9 +313,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Synchronized
|
||||
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
||||
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
||||
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
// Init advanced unlock prompt
|
||||
if (biometricPrompt == null) {
|
||||
biometricPrompt = BiometricPrompt(retrieveContext(),
|
||||
@@ -311,20 +346,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
|
||||
retrieveContext().startActivityForResult(
|
||||
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
|
||||
REQUEST_DEVICE_CREDENTIAL)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int) {
|
||||
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||
} else {
|
||||
advancedUnlockCallback?.onAuthenticationFailed()
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
deviceCredentialResultLauncher.launch(
|
||||
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +358,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
|
||||
interface AdvancedUnlockErrorCallback {
|
||||
fun onUnrecoverableKeyException(e: Exception)
|
||||
fun onInvalidKeyException(e: Exception)
|
||||
fun onGenericException(e: Exception)
|
||||
}
|
||||
@@ -355,8 +381,6 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||
|
||||
private const val REQUEST_DEVICE_CREDENTIAL = 556
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun canAuthenticate(context: Context): Int {
|
||||
return try {
|
||||
@@ -449,6 +473,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
|
||||
override fun handleDecryptedResult(decryptedValue: String) {}
|
||||
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
advancedCallback.onUnrecoverableKeyException(e)
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
advancedCallback.onInvalidKeyException(e)
|
||||
}
|
||||
@@ -460,6 +488,33 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
deleteKeystoreKey()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
deleteEntryKeyInKeystoreForBiometric(
|
||||
activity,
|
||||
object : AdvancedUnlockErrorCallback {
|
||||
fun showException(e: Exception) {
|
||||
Toast.makeText(activity,
|
||||
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onGenericException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,7 +36,11 @@ abstract class ActionNodeDatabaseRunnable(
|
||||
abstract fun nodeAction()
|
||||
|
||||
override fun onStartRun() {
|
||||
nodeAction()
|
||||
try {
|
||||
nodeAction()
|
||||
} catch (e: Exception) {
|
||||
setError(e)
|
||||
}
|
||||
super.onStartRun()
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ class UpdateGroupRunnable constructor(
|
||||
// Update group with new values
|
||||
mNewGroup.touch(modified = true, touchParents = true)
|
||||
|
||||
if (database.rootGroup == mOldGroup) {
|
||||
database.rootGroup = mNewGroup
|
||||
}
|
||||
// Only change data in index
|
||||
database.updateGroup(mNewGroup)
|
||||
}
|
||||
@@ -50,6 +53,9 @@ class UpdateGroupRunnable constructor(
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, back out changes to global structure
|
||||
if (database.rootGroup == mNewGroup) {
|
||||
database.rootGroup = mOldGroup
|
||||
}
|
||||
database.updateGroup(mOldGroup)
|
||||
}
|
||||
|
||||
|
||||
@@ -192,17 +192,17 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
||||
private val MIN_VERSION = UnsignedInt(0x10)
|
||||
private val MAX_VERSION = UnsignedInt(0x13)
|
||||
|
||||
private val DEFAULT_ITERATIONS = UnsignedLong(2L)
|
||||
private val DEFAULT_ITERATIONS = UnsignedLong(3L)
|
||||
private val MIN_ITERATIONS = UnsignedLong(1L)
|
||||
private val MAX_ITERATIONS = UnsignedLong(4294967295L)
|
||||
|
||||
private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L))
|
||||
private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L * 16L))
|
||||
private val MIN_MEMORY = UnsignedLong(1024L * 8L)
|
||||
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||
private const val MEMORY_BLOCK_SIZE: Long = 1024L
|
||||
|
||||
private val DEFAULT_PARALLELISM = UnsignedInt(2)
|
||||
private val DEFAULT_PARALLELISM = UnsignedInt(4)
|
||||
private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L)
|
||||
private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1))
|
||||
private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
|
||||
pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES))
|
||||
pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME)))
|
||||
pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES))
|
||||
.toLowerCase(Locale.ENGLISH) != "false"
|
||||
.lowercase(Locale.ENGLISH) != "false"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -22,10 +22,10 @@ package com.kunzisoft.keepass.database.element
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
@@ -147,6 +147,10 @@ class Database {
|
||||
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
|
||||
}
|
||||
|
||||
fun updateCustomIcon(customIcon: IconImageCustom) {
|
||||
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
|
||||
}
|
||||
|
||||
fun getTemplates(templateCreation: Boolean): List<Template> {
|
||||
return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf()
|
||||
}
|
||||
@@ -222,31 +226,33 @@ class Database {
|
||||
mDatabaseKDBX?.descriptionChanged = DateInstant()
|
||||
}
|
||||
|
||||
val allowDefaultUsername: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
|
||||
|
||||
var defaultUsername: String
|
||||
get() {
|
||||
return mDatabaseKDBX?.defaultUserName ?: "" // TODO mDatabaseKDB default username
|
||||
return mDatabaseKDB?.defaultUserName ?: mDatabaseKDBX?.defaultUserName ?: ""
|
||||
}
|
||||
set(username) {
|
||||
mDatabaseKDB?.defaultUserName = username
|
||||
mDatabaseKDBX?.defaultUserName = username
|
||||
mDatabaseKDBX?.defaultUserNameChanged = DateInstant()
|
||||
}
|
||||
|
||||
val allowCustomColor: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
|
||||
|
||||
// with format "#000000"
|
||||
var customColor: String
|
||||
var customColor: Int?
|
||||
get() {
|
||||
return mDatabaseKDBX?.color ?: "" // TODO mDatabaseKDB color
|
||||
var colorInt: Int? = null
|
||||
mDatabaseKDBX?.color?.let {
|
||||
try {
|
||||
colorInt = Color.parseColor(it)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
return mDatabaseKDB?.color ?: colorInt
|
||||
}
|
||||
set(value) {
|
||||
// TODO Check color string
|
||||
mDatabaseKDBX?.color = value
|
||||
mDatabaseKDB?.color = value
|
||||
mDatabaseKDBX?.color = if (value == null) {
|
||||
""
|
||||
} else {
|
||||
ChromaUtil.getFormattedColorString(value, false)
|
||||
}
|
||||
}
|
||||
|
||||
val allowOTP: Boolean
|
||||
@@ -360,7 +366,7 @@ class Database {
|
||||
mDatabaseKDBX?.masterKey = masterKey
|
||||
}
|
||||
|
||||
val rootGroup: Group?
|
||||
var rootGroup: Group?
|
||||
get() {
|
||||
mDatabaseKDB?.rootGroup?.let {
|
||||
return Group(it)
|
||||
@@ -370,6 +376,25 @@ class Database {
|
||||
}
|
||||
return null
|
||||
}
|
||||
set(value) {
|
||||
value?.groupKDB?.let { rootKDB ->
|
||||
mDatabaseKDB?.rootGroup = rootKDB
|
||||
}
|
||||
value?.groupKDBX?.let { rootKDBX ->
|
||||
mDatabaseKDBX?.rootGroup = rootKDBX
|
||||
}
|
||||
}
|
||||
|
||||
val rootGroupIsVirtual: Boolean
|
||||
get() {
|
||||
mDatabaseKDB?.let {
|
||||
return true
|
||||
}
|
||||
mDatabaseKDBX?.let {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not modify groups here, used for read only
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
@@ -238,6 +240,42 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
entryKDBX?.notes = value
|
||||
}
|
||||
|
||||
var backgroundColor: Int?
|
||||
get() {
|
||||
var colorInt: Int? = null
|
||||
entryKDBX?.backgroundColor?.let {
|
||||
try {
|
||||
colorInt = Color.parseColor(it)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
return colorInt
|
||||
}
|
||||
set(value) {
|
||||
entryKDBX?.backgroundColor = if (value == null) {
|
||||
""
|
||||
} else {
|
||||
ChromaUtil.getFormattedColorString(value, false)
|
||||
}
|
||||
}
|
||||
|
||||
var foregroundColor: Int?
|
||||
get() {
|
||||
var colorInt: Int? = null
|
||||
entryKDBX?.foregroundColor?.let {
|
||||
try {
|
||||
colorInt = Color.parseColor(it)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
return colorInt
|
||||
}
|
||||
set(value) {
|
||||
entryKDBX?.foregroundColor = if (value == null) {
|
||||
""
|
||||
} else {
|
||||
ChromaUtil.getFormattedColorString(value, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTan(): Boolean {
|
||||
return title == PMS_TAN_ENTRY && username.isNotEmpty()
|
||||
}
|
||||
@@ -419,6 +457,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
entryInfo.expiryTime = expiryTime
|
||||
entryInfo.url = url
|
||||
entryInfo.notes = notes
|
||||
entryInfo.backgroundColor = backgroundColor
|
||||
entryInfo.foregroundColor = foregroundColor
|
||||
entryInfo.customFields = getExtraFields().toMutableList()
|
||||
// Add otpElement to generate token
|
||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||
@@ -453,6 +493,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
expiryTime = newEntryInfo.expiryTime
|
||||
url = newEntryInfo.url
|
||||
notes = newEntryInfo.notes
|
||||
backgroundColor = newEntryInfo.backgroundColor
|
||||
foregroundColor = newEntryInfo.foregroundColor
|
||||
addExtraFields(newEntryInfo.customFields)
|
||||
database?.attachmentPool?.let { binaryPool ->
|
||||
newEntryInfo.attachments.forEach { attachment ->
|
||||
|
||||
@@ -31,6 +31,7 @@ import com.kunzisoft.keepass.database.element.node.*
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@@ -308,8 +309,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
|
||||
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
|
||||
|
||||
// TODO Change KDB parser to remove meta entries
|
||||
return groupKDB?.getChildEntries()?.filter {
|
||||
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
|
||||
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream()))
|
||||
&& (!it.isCurrentlyExpires or showExpiredEntries)
|
||||
}?.map {
|
||||
Entry(it)
|
||||
@@ -453,6 +455,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
|
||||
fun getGroupInfo(): GroupInfo {
|
||||
val groupInfo = GroupInfo()
|
||||
groupInfo.id = groupKDBX?.nodeId?.id
|
||||
groupInfo.title = title
|
||||
groupInfo.icon = icon
|
||||
groupInfo.creationTime = creationTime
|
||||
|
||||
@@ -31,6 +31,10 @@ class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(bi
|
||||
return newUUID
|
||||
}
|
||||
|
||||
fun getCustomIcon(key: UUID): IconImageCustom? {
|
||||
return customIcons[key]
|
||||
}
|
||||
|
||||
fun any(predicate: (IconImageCustom)-> Boolean): Boolean {
|
||||
return customIcons.any { predicate(it.value) }
|
||||
}
|
||||
|
||||
@@ -41,18 +41,16 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||
|
||||
override val version: String
|
||||
get() = "KeePass 1"
|
||||
get() = "V1"
|
||||
|
||||
init {
|
||||
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
||||
rootGroup = createGroup().apply {
|
||||
icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID)
|
||||
}
|
||||
kdfListV3.add(KdfFactory.aesKdf)
|
||||
}
|
||||
|
||||
private fun getGroupById(groupId: Int): GroupKDB? {
|
||||
if (groupId == -1)
|
||||
return null
|
||||
return getGroupById(NodeIdInt(groupId))
|
||||
}
|
||||
|
||||
val backupGroup: GroupKDB?
|
||||
get() {
|
||||
return retrieveBackup()
|
||||
@@ -63,6 +61,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
return listOf(BACKUP_FOLDER_TITLE)
|
||||
}
|
||||
|
||||
var defaultUserName: String = ""
|
||||
|
||||
var color: Int? = null
|
||||
|
||||
override val kdfEngine: KdfEngine
|
||||
get() = kdfListV3[0]
|
||||
|
||||
@@ -77,11 +79,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
return list
|
||||
}
|
||||
|
||||
val rootGroups: List<GroupKDB>
|
||||
get() {
|
||||
return rootGroup?.getChildGroups() ?: ArrayList()
|
||||
}
|
||||
|
||||
override val passwordEncoding: String
|
||||
get() = "ISO-8859-1"
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
FILE_VERSION_41 -> "4.1"
|
||||
else -> "UNKNOWN"
|
||||
}
|
||||
return "KeePass 2 - KDBX$kdbxStringVersion"
|
||||
return "V2 - KDBX$kdbxStringVersion"
|
||||
}
|
||||
|
||||
override val kdfEngine: KdfEngine?
|
||||
@@ -414,37 +414,37 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
}
|
||||
|
||||
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.decodeTitleKey(recursionLevel).equals(title, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.decodeUsernameKey(recursionLevel).equals(username, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.decodeUrlKey(recursionLevel).equals(url, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.decodePasswordKey(recursionLevel).equals(password, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.decodeNotesKey(recursionLevel).equals(notes, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
|
||||
return entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.customData.containsItemWithValue(customDataValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ abstract class DatabaseVersioned<
|
||||
var changeDuplicateId = false
|
||||
|
||||
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
|
||||
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
||||
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
||||
|
||||
abstract val version: String
|
||||
|
||||
@@ -89,6 +89,7 @@ abstract class DatabaseVersioned<
|
||||
set(value) {
|
||||
field = value
|
||||
value?.let {
|
||||
removeGroupIndex(it)
|
||||
addGroupIndex(it)
|
||||
}
|
||||
}
|
||||
@@ -124,25 +125,29 @@ abstract class DatabaseVersioned<
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||
val keyData = keyInputStream.readBytes()
|
||||
try {
|
||||
val keyData = keyInputStream.readBytes()
|
||||
|
||||
// Check XML key file
|
||||
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
||||
if (xmlKeyByteArray != null) {
|
||||
return xmlKeyByteArray
|
||||
}
|
||||
|
||||
// Check 32 bytes key file
|
||||
when (keyData.size) {
|
||||
32 -> return keyData
|
||||
64 -> try {
|
||||
return Hex.decodeHex(String(keyData).toCharArray())
|
||||
} catch (ignoredException: Exception) {
|
||||
// Key is not base 64, treat it as binary data
|
||||
// Check XML key file
|
||||
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
||||
if (xmlKeyByteArray != null) {
|
||||
return xmlKeyByteArray
|
||||
}
|
||||
|
||||
// Check 32 bytes key file
|
||||
when (keyData.size) {
|
||||
32 -> return keyData
|
||||
64 -> try {
|
||||
return Hex.decodeHex(String(keyData).toCharArray())
|
||||
} catch (ignoredException: Exception) {
|
||||
// Key is not base 64, treat it as binary data
|
||||
}
|
||||
}
|
||||
// Hash file as binary data
|
||||
return HashManager.hashSha256(keyData)
|
||||
} catch (outOfMemoryError: OutOfMemoryError) {
|
||||
throw IOException("Keyfile data is too large", outOfMemoryError)
|
||||
}
|
||||
// Hash file as binary data
|
||||
return HashManager.hashSha256(keyData)
|
||||
}
|
||||
|
||||
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||
@@ -194,12 +199,6 @@ abstract class DatabaseVersioned<
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
fun doForEachGroupInIndex(action: (Group) -> Unit) {
|
||||
for (group in groupIndexes) {
|
||||
action.invoke(group.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an id number is already in use
|
||||
*
|
||||
@@ -215,14 +214,7 @@ abstract class DatabaseVersioned<
|
||||
return groupIndexes.values
|
||||
}
|
||||
|
||||
fun setGroupIndexes(groupList: List<Group>) {
|
||||
this.groupIndexes.clear()
|
||||
for (currentGroup in groupList) {
|
||||
this.groupIndexes[currentGroup.nodeId] = currentGroup
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupById(id: NodeId<GroupId>): Group? {
|
||||
open fun getGroupById(id: NodeId<GroupId>): Group? {
|
||||
return this.groupIndexes[id]
|
||||
}
|
||||
|
||||
@@ -246,16 +238,6 @@ abstract class DatabaseVersioned<
|
||||
this.groupIndexes.remove(group.nodeId)
|
||||
}
|
||||
|
||||
fun numberOfGroups(): Int {
|
||||
return groupIndexes.size
|
||||
}
|
||||
|
||||
fun doForEachEntryInIndex(action: (Entry) -> Unit) {
|
||||
for (entry in entryIndexes) {
|
||||
action.invoke(entry.value)
|
||||
}
|
||||
}
|
||||
|
||||
fun isEntryIdUsed(id: NodeId<EntryId>): Boolean {
|
||||
return entryIndexes.containsKey(id)
|
||||
}
|
||||
@@ -268,6 +250,10 @@ abstract class DatabaseVersioned<
|
||||
return this.entryIndexes[id]
|
||||
}
|
||||
|
||||
fun findEntry(predicate: (Entry) -> Boolean): Entry? {
|
||||
return this.entryIndexes.values.find(predicate)
|
||||
}
|
||||
|
||||
fun addEntryIndex(entry: Entry) {
|
||||
val entryId = entry.nodeId
|
||||
if (entryIndexes.containsKey(entryId)) {
|
||||
@@ -288,10 +274,6 @@ abstract class DatabaseVersioned<
|
||||
this.entryIndexes.remove(entry.nodeId)
|
||||
}
|
||||
|
||||
fun numberOfEntries(): Int {
|
||||
return entryIndexes.size
|
||||
}
|
||||
|
||||
open fun clearCache() {
|
||||
this.groupIndexes.clear()
|
||||
this.entryIndexes.clear()
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
@@ -60,18 +61,43 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
private var binaryDataId: Int? = null
|
||||
|
||||
// Determine if this is a MetaStream entry
|
||||
val isMetaStream: Boolean
|
||||
get() {
|
||||
if (notes.isEmpty()) return false
|
||||
if (binaryDescription != PMS_ID_BINDESC) return false
|
||||
if (title.isEmpty()) return false
|
||||
if (title != PMS_ID_TITLE) return false
|
||||
if (username.isEmpty()) return false
|
||||
if (username != PMS_ID_USER) return false
|
||||
if (url.isEmpty()) return false
|
||||
if (url != PMS_ID_URL) return false
|
||||
return icon.standard.id == KEY_ID
|
||||
}
|
||||
fun isMetaStream(): Boolean {
|
||||
if (notes.isEmpty()) return false
|
||||
if (binaryDescription != PMS_ID_BINDESC) return false
|
||||
if (title.isEmpty()) return false
|
||||
if (title != PMS_ID_TITLE) return false
|
||||
if (username.isEmpty()) return false
|
||||
if (username != PMS_ID_USER) return false
|
||||
if (url.isEmpty()) return false
|
||||
if (url != PMS_ID_URL) return false
|
||||
return icon.standard.id == KEY_ID
|
||||
}
|
||||
|
||||
fun isMetaStreamDefaultUsername(): Boolean {
|
||||
return isMetaStream() && notes == PMS_STREAM_DEFAULTUSER
|
||||
}
|
||||
|
||||
private fun setMetaStream() {
|
||||
binaryDescription = PMS_ID_BINDESC
|
||||
title = PMS_ID_TITLE
|
||||
username = PMS_ID_USER
|
||||
url = PMS_ID_URL
|
||||
icon.standard = IconImageStandard(KEY_ID)
|
||||
}
|
||||
|
||||
fun setMetaStreamDefaultUsername() {
|
||||
notes = PMS_STREAM_DEFAULTUSER
|
||||
setMetaStream()
|
||||
}
|
||||
|
||||
fun isMetaStreamDatabaseColor(): Boolean {
|
||||
return isMetaStream() && notes == PMS_STREAM_DBCOLOR
|
||||
}
|
||||
|
||||
fun setMetaStreamDatabaseColor() {
|
||||
notes = PMS_STREAM_DBCOLOR
|
||||
setMetaStream()
|
||||
}
|
||||
|
||||
override fun initNodeId(): NodeId<UUID> {
|
||||
return NodeIdUUID()
|
||||
@@ -184,6 +210,13 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
private const val PMS_ID_USER = "SYSTEM"
|
||||
private const val PMS_ID_URL = "$"
|
||||
|
||||
const val PMS_STREAM_SIMPLESTATE = "Simple UI State"
|
||||
const val PMS_STREAM_DEFAULTUSER = "Default User Name"
|
||||
const val PMS_STREAM_SEARCHHISTORYITEM = "Search History Item"
|
||||
const val PMS_STREAM_CUSTOMKVP = "Custom KVP"
|
||||
const val PMS_STREAM_DBCOLOR = "Database Color"
|
||||
const val PMS_STREAM_KPXICON2 = "KPX_CUSTOM_ICONS_2"
|
||||
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<EntryKDB> = object : Parcelable.Creator<EntryKDB> {
|
||||
override fun createFromParcel(parcel: Parcel): EntryKDB {
|
||||
|
||||
@@ -32,6 +32,16 @@ class IconImageCustom : IconImageDraw {
|
||||
var name: String = ""
|
||||
var lastModificationTime: DateInstant? = null
|
||||
|
||||
fun updateWith(icon: IconImageCustom) {
|
||||
this.name = icon.name
|
||||
this.lastModificationTime = icon.lastModificationTime
|
||||
}
|
||||
|
||||
constructor(copy: IconImageCustom) {
|
||||
this.uuid = copy.uuid
|
||||
updateWith(copy)
|
||||
}
|
||||
|
||||
constructor(name: String = "", lastModificationTime: DateInstant? = null) {
|
||||
this.uuid = DatabaseVersioned.UUID_ZERO
|
||||
this.name = name
|
||||
|
||||
@@ -81,6 +81,7 @@ class IconImageStandard : IconImageDraw {
|
||||
const val CREDIT_CARD_ID = 37
|
||||
const val TRASH_ID = 43
|
||||
const val FOLDER_ID = 48
|
||||
const val DATABASE_ID = 50
|
||||
const val LIST_ID = 57
|
||||
const val BUILD_ID = 59
|
||||
const val STAR_ID = 61
|
||||
|
||||
@@ -65,7 +65,7 @@ class IconsManager(binaryCache: BinaryCache) {
|
||||
}
|
||||
|
||||
fun getIcon(iconUuid: UUID): IconImageCustom {
|
||||
return IconImageCustom(iconUuid)
|
||||
return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid)
|
||||
}
|
||||
|
||||
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
||||
|
||||
@@ -32,6 +32,19 @@ interface Node: NodeVersionedInterface<Group> {
|
||||
fun removeParent() {
|
||||
parent = null
|
||||
}
|
||||
|
||||
fun getPathString(): String {
|
||||
val pathNodes = mutableListOf<Node>()
|
||||
var currentNode = this
|
||||
pathNodes.add(0, currentNode)
|
||||
while (currentNode.containsParent()) {
|
||||
currentNode.parent?.let { parent ->
|
||||
currentNode = parent
|
||||
pathNodes.add(0, currentNode)
|
||||
}
|
||||
}
|
||||
return pathNodes.joinToString("/") { it.title }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,4 +44,6 @@ abstract class NodeId<Id> : Parcelable {
|
||||
override fun hashCode(): Int {
|
||||
return id?.hashCode() ?: 0
|
||||
}
|
||||
|
||||
abstract fun toVisualString(): String?
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ class NodeIdInt : NodeId<Int> {
|
||||
return id.toString()
|
||||
}
|
||||
|
||||
override fun toVisualString(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<NodeIdInt> = object : Parcelable.Creator<NodeIdInt> {
|
||||
|
||||
@@ -64,6 +64,10 @@ class NodeIdUUID : NodeId<UUID> {
|
||||
return UuidUtil.toHexString(id) ?: id.toString()
|
||||
}
|
||||
|
||||
override fun toVisualString(): String {
|
||||
return toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<NodeIdUUID> = object : Parcelable.Creator<NodeIdUUID> {
|
||||
|
||||
@@ -208,16 +208,8 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
||||
when (attribute.type) {
|
||||
TemplateAttributeType.TEXT -> {
|
||||
try {
|
||||
when (attribute.options.getNumberLines()) {
|
||||
1 -> {
|
||||
// If one line, default attribute option is number of chars
|
||||
attribute.options.setNumberChars(defaultOption.toInt())
|
||||
}
|
||||
else -> {
|
||||
// else it's number of lines
|
||||
attribute.options.setNumberLines(defaultOption.toInt())
|
||||
}
|
||||
}
|
||||
// It's always a number of lines...
|
||||
attribute.options.setNumberLines(defaultOption.toInt())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to transform default text option", e)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ object TemplateField {
|
||||
const val LABEL_DATE_OF_ISSUE = "Date of issue"
|
||||
const val LABEL_EMAIL = "Email"
|
||||
const val LABEL_EMAIL_ADDRESS = "Email address"
|
||||
const val LABEL_WIRELESS = "Wifi"
|
||||
const val LABEL_WIRELESS = "Wi-Fi"
|
||||
const val LABEL_SSID = "SSID"
|
||||
const val LABEL_TYPE = "Type"
|
||||
const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet"
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.file
|
||||
|
||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
|
||||
abstract class DatabaseHeader {
|
||||
|
||||
/**
|
||||
@@ -33,8 +31,4 @@ abstract class DatabaseHeader {
|
||||
*/
|
||||
var encryptionIV = ByteArray(16)
|
||||
|
||||
companion object {
|
||||
val PWM_DBSIG_1 = UnsignedInt(-0x655d26fd)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
||||
*/
|
||||
var transformSeed = ByteArray(32)
|
||||
|
||||
var signature1 = UnsignedInt(0) // = PWM_DBSIG_1
|
||||
var signature1 = UnsignedInt(0) // = DBSIG_1
|
||||
var signature2 = UnsignedInt(0) // = DBSIG_2
|
||||
var flags= UnsignedInt(0)
|
||||
var version= UnsignedInt(0)
|
||||
@@ -84,9 +84,9 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
||||
companion object {
|
||||
|
||||
// DB sig from KeePass 1.03
|
||||
val DBSIG_2 = UnsignedInt(-0x4ab4049b)
|
||||
// DB sig from KeePass 1.03
|
||||
val DBVER_DW = UnsignedInt(0x00030003)
|
||||
val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
|
||||
val DBSIG_2 = UnsignedInt(-0x4ab4049b) // 0xB54BFB65
|
||||
val DBVER_DW = UnsignedInt(0x00030004)
|
||||
|
||||
val FLAG_SHA2 = UnsignedInt(1)
|
||||
val FLAG_RIJNDAEL = UnsignedInt(2)
|
||||
@@ -97,7 +97,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
||||
const val BUF_SIZE = 124
|
||||
|
||||
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
||||
return sig1.toKotlinInt() == PWM_DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt()
|
||||
return sig1.toKotlinInt() == DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt()
|
||||
}
|
||||
|
||||
fun compatibleHeaders(one: UnsignedInt, two: UnsignedInt): Boolean {
|
||||
|
||||
@@ -311,8 +311,9 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
|
||||
companion object {
|
||||
|
||||
val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a)
|
||||
val DBSIG_2 = UnsignedInt(-0x4ab40499)
|
||||
val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
|
||||
val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a) // 0xB54BFB66
|
||||
val DBSIG_2 = UnsignedInt(-0x4ab40499) // 0xB54BFB67
|
||||
|
||||
private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000)
|
||||
val FILE_VERSION_31 = UnsignedInt(0x00030001)
|
||||
@@ -335,7 +336,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
}
|
||||
|
||||
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
||||
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
|
||||
return sig1 == DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
package com.kunzisoft.keepass.database.file.input
|
||||
|
||||
import android.graphics.Color
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
@@ -30,7 +31,6 @@ import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
@@ -98,7 +98,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE))
|
||||
throw IOException("Header corrupted")
|
||||
|
||||
if (header.signature1 != DatabaseHeader.PWM_DBSIG_1
|
||||
if (header.signature1 != DatabaseHeaderKDB.DBSIG_1
|
||||
|| header.signature2 != DatabaseHeaderKDB.DBSIG_2) {
|
||||
throw SignatureDatabaseException()
|
||||
}
|
||||
@@ -153,10 +153,6 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
)
|
||||
)
|
||||
|
||||
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
||||
val newRoot = mDatabase.createGroup()
|
||||
mDatabase.rootGroup = newRoot
|
||||
|
||||
// Import all nodes
|
||||
val groupLevelList = HashMap<GroupKDB, Int>()
|
||||
var newGroup: GroupKDB? = null
|
||||
@@ -303,7 +299,34 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
newGroup = null
|
||||
}
|
||||
newEntry?.let { entry ->
|
||||
mDatabase.addEntryIndex(entry)
|
||||
// Parse meta info
|
||||
when {
|
||||
entry.isMetaStreamDefaultUsername() -> {
|
||||
var defaultUser = ""
|
||||
entry.getBinary(mDatabase.attachmentPool)
|
||||
?.getInputDataStream(mDatabase.binaryCache)?.use {
|
||||
defaultUser = String(it.readBytes())
|
||||
}
|
||||
mDatabase.defaultUserName = defaultUser
|
||||
}
|
||||
entry.isMetaStreamDatabaseColor() -> {
|
||||
var color: Int? = null
|
||||
entry.getBinary(mDatabase.attachmentPool)
|
||||
?.getInputDataStream(mDatabase.binaryCache)?.use {
|
||||
val reverseColor = UnsignedInt(it.readBytes4ToUInt()).toKotlinInt()
|
||||
color = Color.rgb(
|
||||
Color.blue(reverseColor),
|
||||
Color.green(reverseColor),
|
||||
Color.red(reverseColor)
|
||||
)
|
||||
}
|
||||
mDatabase.color = color
|
||||
}
|
||||
// TODO manager other meta stream
|
||||
else -> {
|
||||
mDatabase.addEntryIndex(entry)
|
||||
}
|
||||
}
|
||||
currentEntryNumber++
|
||||
newEntry = null
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
||||
@Throws(IOException::class)
|
||||
fun output() {
|
||||
|
||||
mos.write4BytesUInt(DatabaseHeader.PWM_DBSIG_1)
|
||||
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_1)
|
||||
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
|
||||
mos.write4BytesUInt(header.version)
|
||||
|
||||
@@ -130,6 +130,6 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EndHeaderValue = byteArrayOf('\r'.toByte(), '\n'.toByte(), '\r'.toByte(), '\n'.toByte())
|
||||
private val EndHeaderValue = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte(), '\r'.code.toByte(), '\n'.code.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.file.output
|
||||
|
||||
import android.graphics.Color
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
@@ -34,7 +36,6 @@ import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.*
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherOutputStream
|
||||
|
||||
@@ -44,6 +45,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
|
||||
private var headerHashBlock: ByteArray? = null
|
||||
|
||||
private var mGroupList = mutableListOf<GroupKDB>()
|
||||
private var mEntryList = mutableListOf<EntryKDB>()
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
fun getFinalKey(header: DatabaseHeader): ByteArray? {
|
||||
try {
|
||||
@@ -61,7 +65,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
// and remove any orphaned nodes that are no longer part of the tree hierarchy
|
||||
// also remove the virtual root not present in kdb
|
||||
val rootGroup = mDatabaseKDB.rootGroup
|
||||
sortGroupsForOutput()
|
||||
sortNodesForOutput()
|
||||
|
||||
val header = outputHeader(mOutputStream)
|
||||
|
||||
@@ -91,6 +95,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
} finally {
|
||||
// Add again the virtual root group for better management
|
||||
mDatabaseKDB.rootGroup = rootGroup
|
||||
clearParser()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +110,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
|
||||
// Build header
|
||||
val header = DatabaseHeaderKDB()
|
||||
header.signature1 = DatabaseHeader.PWM_DBSIG_1
|
||||
header.signature1 = DatabaseHeaderKDB.DBSIG_1
|
||||
header.signature2 = DatabaseHeaderKDB.DBSIG_2
|
||||
header.flags = DatabaseHeaderKDB.FLAG_SHA2
|
||||
|
||||
@@ -120,8 +125,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
header.version = DatabaseHeaderKDB.DBVER_DW
|
||||
header.numGroups = UnsignedInt(mDatabaseKDB.numberOfGroups())
|
||||
header.numEntries = UnsignedInt(mDatabaseKDB.numberOfEntries())
|
||||
// To remove root
|
||||
header.numGroups = UnsignedInt(mGroupList.size)
|
||||
header.numEntries = UnsignedInt(mEntryList.size)
|
||||
header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds)
|
||||
|
||||
setIVs(header)
|
||||
@@ -194,31 +200,89 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
// Groups
|
||||
mDatabaseKDB.doForEachGroupInIndex { group ->
|
||||
GroupOutputKDB(group, outputStream).output()
|
||||
mGroupList.forEach { group ->
|
||||
if (group != mDatabaseKDB.rootGroup) {
|
||||
GroupOutputKDB(group, outputStream).output()
|
||||
}
|
||||
}
|
||||
// Entries
|
||||
mDatabaseKDB.doForEachEntryInIndex { entry ->
|
||||
mEntryList.forEach { entry ->
|
||||
EntryOutputKDB(mDatabaseKDB, entry, outputStream).output()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortGroupsForOutput() {
|
||||
val groupList = ArrayList<GroupKDB>()
|
||||
// Rebuild list according to sorting order removing any orphaned groups
|
||||
for (rootGroup in mDatabaseKDB.rootGroups) {
|
||||
sortGroup(rootGroup, groupList)
|
||||
}
|
||||
mDatabaseKDB.setGroupIndexes(groupList)
|
||||
private fun clearParser() {
|
||||
mGroupList.clear()
|
||||
mEntryList.clear()
|
||||
}
|
||||
|
||||
private fun sortGroup(group: GroupKDB, groupList: MutableList<GroupKDB>) {
|
||||
private fun sortNodesForOutput() {
|
||||
clearParser()
|
||||
// Rebuild list according to sorting order removing any orphaned groups
|
||||
// Do not keep root
|
||||
mDatabaseKDB.rootGroup?.getChildGroups()?.let { rootSubGroups ->
|
||||
for (rootGroup in rootSubGroups) {
|
||||
sortGroup(rootGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortGroup(group: GroupKDB) {
|
||||
// Add current tree
|
||||
groupList.add(group)
|
||||
mGroupList.add(group)
|
||||
|
||||
for (childEntry in group.getChildEntries()) {
|
||||
if (!childEntry.isMetaStreamDefaultUsername()
|
||||
&& !childEntry.isMetaStreamDatabaseColor()) {
|
||||
mEntryList.add(childEntry)
|
||||
}
|
||||
}
|
||||
|
||||
// Add MetaStream
|
||||
if (mDatabaseKDB.defaultUserName.isNotEmpty()) {
|
||||
val metaEntry = EntryKDB().apply {
|
||||
setMetaStreamDefaultUsername()
|
||||
setDefaultUsername(this)
|
||||
}
|
||||
mDatabaseKDB.addEntryTo(metaEntry, group)
|
||||
mEntryList.add(metaEntry)
|
||||
}
|
||||
if (mDatabaseKDB.color != null) {
|
||||
val metaEntry = EntryKDB().apply {
|
||||
setMetaStreamDatabaseColor()
|
||||
setDatabaseColor(this)
|
||||
}
|
||||
mDatabaseKDB.addEntryTo(metaEntry, group)
|
||||
mEntryList.add(metaEntry)
|
||||
}
|
||||
|
||||
// Recurse over children
|
||||
for (childGroup in group.getChildGroups()) {
|
||||
sortGroup(childGroup, groupList)
|
||||
sortGroup(childGroup)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDefaultUsername(entryKDB: EntryKDB) {
|
||||
val binaryData = mDatabaseKDB.buildNewAttachment()
|
||||
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
|
||||
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
|
||||
outputStream.write(mDatabaseKDB.defaultUserName.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDatabaseColor(entryKDB: EntryKDB) {
|
||||
val binaryData = mDatabaseKDB.buildNewAttachment()
|
||||
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
|
||||
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
|
||||
var reversColor = Color.BLACK
|
||||
mDatabaseKDB.color?.let {
|
||||
reversColor = Color.rgb(
|
||||
Color.blue(it),
|
||||
Color.green(it),
|
||||
Color.red(it)
|
||||
)
|
||||
}
|
||||
outputStream.write4BytesUInt(UnsignedInt(reversColor))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
||||
import com.kunzisoft.keepass.stream.HmacBlockOutputStream
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import org.joda.time.DateTime
|
||||
import org.xmlpull.v1.XmlSerializer
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
@@ -765,7 +764,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
var character: Char
|
||||
for (element in text) {
|
||||
character = element
|
||||
val hexChar = character.toInt()
|
||||
val hexChar = character.code
|
||||
if (
|
||||
hexChar in 0x20..0xD7FF ||
|
||||
hexChar == 0x9 ||
|
||||
|
||||
@@ -119,7 +119,7 @@ class GroupActivityEducation(activity: Activity)
|
||||
.outerCircleColorInt(getCircleColor())
|
||||
.outerCircleAlpha(getCircleAlpha())
|
||||
.textColorInt(getTextColor())
|
||||
.tintTarget(true)
|
||||
.tintTarget(false)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
|
||||
@@ -38,9 +38,9 @@ class PasswordActivityEducation(activity: Activity)
|
||||
activity.getString(R.string.education_unlock_summary))
|
||||
.outerCircleColorInt(getCircleColor())
|
||||
.outerCircleAlpha(getCircleAlpha())
|
||||
.icon(ContextCompat.getDrawable(activity, R.mipmap.ic_launcher_round))
|
||||
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_lock_open_white_24dp))
|
||||
.textColorInt(getTextColor())
|
||||
.tintTarget(false)
|
||||
.tintTarget(true)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
|
||||
@@ -40,6 +40,8 @@ class EntryInfo : NodeInfo {
|
||||
var password: String = ""
|
||||
var url: String = ""
|
||||
var notes: String = ""
|
||||
var backgroundColor: Int? = null
|
||||
var foregroundColor: Int? = null
|
||||
var customFields: MutableList<Field> = mutableListOf()
|
||||
var attachments: MutableList<Attachment> = mutableListOf()
|
||||
var otpModel: OtpModel? = null
|
||||
@@ -53,6 +55,10 @@ class EntryInfo : NodeInfo {
|
||||
password = parcel.readString() ?: password
|
||||
url = parcel.readString() ?: url
|
||||
notes = parcel.readString() ?: notes
|
||||
val readBgColor = parcel.readInt()
|
||||
backgroundColor = if (readBgColor == -1) null else readBgColor
|
||||
val readFgColor = parcel.readInt()
|
||||
foregroundColor = if (readFgColor == -1) null else readFgColor
|
||||
parcel.readList(customFields, Field::class.java.classLoader)
|
||||
parcel.readList(attachments, Attachment::class.java.classLoader)
|
||||
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
||||
@@ -70,6 +76,8 @@ class EntryInfo : NodeInfo {
|
||||
parcel.writeString(password)
|
||||
parcel.writeString(url)
|
||||
parcel.writeString(notes)
|
||||
parcel.writeInt(backgroundColor ?: -1)
|
||||
parcel.writeInt(foregroundColor ?: -1)
|
||||
parcel.writeList(customFields)
|
||||
parcel.writeList(attachments)
|
||||
parcel.writeParcelable(otpModel, flags)
|
||||
@@ -196,6 +204,8 @@ class EntryInfo : NodeInfo {
|
||||
if (password != other.password) return false
|
||||
if (url != other.url) return false
|
||||
if (notes != other.notes) return false
|
||||
if (backgroundColor != other.backgroundColor) return false
|
||||
if (foregroundColor != other.foregroundColor) return false
|
||||
if (customFields != other.customFields) return false
|
||||
if (attachments != other.attachments) return false
|
||||
if (otpModel != other.otpModel) return false
|
||||
@@ -211,6 +221,8 @@ class EntryInfo : NodeInfo {
|
||||
result = 31 * result + password.hashCode()
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + notes.hashCode()
|
||||
result = 31 * result + backgroundColor.hashCode()
|
||||
result = 31 * result + foregroundColor.hashCode()
|
||||
result = 31 * result + customFields.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + (otpModel?.hashCode() ?: 0)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.ParcelUuid
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID
|
||||
import java.util.*
|
||||
|
||||
class GroupInfo : NodeInfo {
|
||||
|
||||
var id: UUID? = null
|
||||
var notes: String? = null
|
||||
|
||||
init {
|
||||
@@ -16,11 +19,14 @@ class GroupInfo : NodeInfo {
|
||||
constructor(): super()
|
||||
|
||||
constructor(parcel: Parcel): super(parcel) {
|
||||
id = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: id
|
||||
notes = parcel.readString()
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
super.writeToParcel(parcel, flags)
|
||||
val uuid = if (id != null) ParcelUuid(id) else null
|
||||
parcel.writeParcelable(uuid, flags)
|
||||
parcel.writeString(notes)
|
||||
}
|
||||
|
||||
@@ -29,6 +35,7 @@ class GroupInfo : NodeInfo {
|
||||
if (other !is GroupInfo) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (notes != other.notes) return false
|
||||
|
||||
return true
|
||||
@@ -36,6 +43,7 @@ class GroupInfo : NodeInfo {
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + (id?.hashCode() ?: 0)
|
||||
result = 31 * result + (notes?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import java.util.*
|
||||
|
||||
open class NodeInfo() : Parcelable {
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
||||
|
||||
fun replaceBase32Chars(parameter: String): String {
|
||||
// Add padding '=' at end if not Base32 length
|
||||
var parameterNewSize = parameter.toUpperCase(Locale.ENGLISH).removeSpaceChars()
|
||||
var parameterNewSize = parameter.uppercase(Locale.ENGLISH).removeSpaceChars()
|
||||
while (parameterNewSize.length % 8 != 0) {
|
||||
parameterNewSize += '='
|
||||
}
|
||||
@@ -264,7 +264,7 @@ enum class OtpTokenType {
|
||||
|
||||
companion object {
|
||||
fun getFromString(tokenType: String): OtpTokenType {
|
||||
return when (tokenType.toLowerCase(Locale.ENGLISH)) {
|
||||
return when (tokenType.lowercase(Locale.ENGLISH)) {
|
||||
"s", "steam" -> STEAM
|
||||
"hotp" -> RFC4226
|
||||
else -> RFC6238
|
||||
|
||||
@@ -143,7 +143,7 @@ object OtpEntryFields {
|
||||
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
|
||||
val uri = Uri.parse(otpPlainText.removeSpaceChars())
|
||||
|
||||
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
|
||||
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.lowercase(Locale.ENGLISH)) {
|
||||
Log.e(TAG, "Invalid or missing scheme in uri")
|
||||
return false
|
||||
}
|
||||
@@ -309,7 +309,7 @@ object OtpEntryFields {
|
||||
}
|
||||
if (algorithmField != null) {
|
||||
otpElement.algorithm =
|
||||
when (algorithmField.toUpperCase(Locale.ENGLISH)) {
|
||||
when (algorithmField.uppercase(Locale.ENGLISH)) {
|
||||
TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1
|
||||
TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256
|
||||
TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512
|
||||
@@ -417,7 +417,7 @@ object OtpEntryFields {
|
||||
val output = HashMap<String, String>()
|
||||
for (element in elements) {
|
||||
val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
output[pair[0].toLowerCase(Locale.ENGLISH)] = pair[1]
|
||||
output[pair[0].lowercase(Locale.ENGLISH)] = pair[1]
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.kunzisoft.keepass.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
@@ -50,11 +52,20 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
mTempCipherDao = ArrayList()
|
||||
}
|
||||
|
||||
// It's simpler to use pendingIntent to perform REMOVE_ADVANCED_UNLOCK_KEY_ACTION
|
||||
// because can be directly broadcast to another module or app
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
|
||||
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
|
||||
4577, Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION), 0)
|
||||
4577,
|
||||
Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
})
|
||||
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
|
||||
val notificationBuilder = buildNewNotification().apply {
|
||||
setSmallIcon(if (biometricUnlockEnabled) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.ContentResolver
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -188,20 +189,30 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
||||
|
||||
val pendingContentIntent = PendingIntent.getActivity(this,
|
||||
0,
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(attachmentNotification.uri,
|
||||
contentResolver.getType(attachmentNotification.uri))
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
0,
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(attachmentNotification.uri,
|
||||
contentResolver.getType(attachmentNotification.uri))
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
}
|
||||
)
|
||||
|
||||
val pendingDeleteIntent = PendingIntent.getService(this,
|
||||
0,
|
||||
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
||||
// No action to delete the service
|
||||
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
}
|
||||
)
|
||||
|
||||
val fileName = UriUtil.getFileData(this, attachmentNotification.uri)?.name
|
||||
?: attachmentNotification.uri.path
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.services
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
@@ -112,7 +113,13 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
putParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS, fieldsToAdd)
|
||||
}
|
||||
return PendingIntent.getService(
|
||||
this, 0, copyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
this, 0, copyIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) {
|
||||
@@ -162,7 +169,13 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
val cleanIntent = Intent(this, ClipboardEntryNotificationService::class.java)
|
||||
cleanIntent.action = ACTION_CLEAN_CLIPBOARD
|
||||
val cleanPendingIntent = PendingIntent.getService(
|
||||
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
this, 0, cleanIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
)
|
||||
builder.setDeleteIntent(cleanPendingIntent)
|
||||
|
||||
//Get settings
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.media.app.NotificationCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
@@ -407,11 +408,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
this,
|
||||
0,
|
||||
Intent(this, GroupActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
)
|
||||
val pendingDeleteIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
4576, Intent(LOCK_ACTION), 0
|
||||
4576,
|
||||
Intent(LOCK_ACTION),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
// Add actions in notifications
|
||||
notificationBuilder.apply {
|
||||
@@ -420,9 +431,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
// Unfortunately swipe is disabled in lollipop+
|
||||
setDeleteIntent(pendingDeleteIntent)
|
||||
addAction(
|
||||
R.drawable.ic_lock_white_24dp, getString(R.string.lock),
|
||||
R.drawable.ic_lock_database_white_32dp, getString(R.string.lock),
|
||||
pendingDeleteIntent
|
||||
)
|
||||
// Won't work with Xiaomi and Kitkat
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||
setStyle(
|
||||
NotificationCompat.MediaStyle()
|
||||
.setShowActionsInCompactView(0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.services
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.preference.PreferenceManager
|
||||
@@ -93,7 +94,13 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
||||
val deleteIntent = Intent(this, KeyboardEntryNotificationService::class.java).apply {
|
||||
action = ACTION_CLEAN_KEYBOARD_ENTRY
|
||||
}
|
||||
pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
)
|
||||
|
||||
val builder = buildNewNotification()
|
||||
.setSmallIcon(R.drawable.notification_ic_keyboard_key_24dp)
|
||||
|
||||
@@ -57,6 +57,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
@Suppress("DEPRECATION")
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
@Suppress("DEPRECATION")
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
@@ -157,7 +156,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
|
||||
intent.data = Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService")
|
||||
Log.d(javaClass.name, "Autofill enable service: intent=$intent")
|
||||
startActivityForResult(intent, REQUEST_CODE_AUTOFILL)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Log.d(javaClass.name, "Autofill service already enabled.")
|
||||
}
|
||||
@@ -366,26 +365,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
) { _, _ ->
|
||||
validate?.invoke()
|
||||
deleteKeysAlertDialog?.setOnDismissListener(null)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.deleteEntryKeyInKeystoreForBiometric(
|
||||
activity,
|
||||
object : AdvancedUnlockManager.AdvancedUnlockErrorCallback {
|
||||
fun showException(e: Exception) {
|
||||
Toast.makeText(context,
|
||||
getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onGenericException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
||||
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
||||
}
|
||||
.setNegativeButton(resources.getString(android.R.string.cancel)
|
||||
) { _, _ ->}
|
||||
@@ -472,7 +452,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
getString(R.string.show_uuid_key),
|
||||
getString(R.string.enable_education_screens_key),
|
||||
getString(R.string.reset_education_screens_key) -> {
|
||||
DATABASE_APPEARANCE_PREFERENCE_CHANGED = true
|
||||
DATABASE_PREFERENCE_CHANGED = true
|
||||
}
|
||||
}
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
@@ -494,6 +474,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
@Suppress("DEPRECATION")
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||
}
|
||||
@@ -533,9 +514,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CODE_AUTOFILL = 5201
|
||||
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
||||
|
||||
var DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
|
||||
var DATABASE_PREFERENCE_CHANGED = false
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,8 @@ import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.SwitchPreference
|
||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
@@ -57,7 +57,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
|
||||
private var dbNamePref: InputTextPreference? = null
|
||||
private var dbDescriptionPref: InputTextPreference? = null
|
||||
private var dbDefaultUsername: InputTextPreference? = null
|
||||
private var dbDefaultUsernamePref: InputTextPreference? = null
|
||||
private var dbCustomColorPref: DialogColorPreference? = null
|
||||
private var dbDataCompressionPref: Preference? = null
|
||||
private var recycleBinGroupPref: DialogListExplanationPreference? = null
|
||||
@@ -164,29 +164,20 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
|
||||
// Database default username
|
||||
dbDefaultUsername = findPreference(getString(R.string.database_default_username_key))
|
||||
if (database.allowDefaultUsername) {
|
||||
dbDefaultUsername?.summary = database.defaultUsername
|
||||
} else {
|
||||
dbDefaultUsername?.isEnabled = false
|
||||
// TODO dbGeneralPrefCategory?.removePreference(dbDefaultUsername)
|
||||
}
|
||||
dbDefaultUsernamePref = findPreference(getString(R.string.database_default_username_key))
|
||||
dbDefaultUsernamePref?.summary = database.defaultUsername
|
||||
|
||||
// Database custom color
|
||||
dbCustomColorPref = findPreference(getString(R.string.database_custom_color_key))
|
||||
if (database.allowCustomColor) {
|
||||
dbCustomColorPref?.apply {
|
||||
try {
|
||||
color = Color.parseColor(database.customColor)
|
||||
summary = database.customColor
|
||||
} catch (e: Exception) {
|
||||
color = DialogColorPreference.DISABLE_COLOR
|
||||
summary = ""
|
||||
}
|
||||
dbCustomColorPref?.apply {
|
||||
val customColor = database.customColor
|
||||
if (customColor != null) {
|
||||
color = customColor
|
||||
summary = ChromaUtil.getFormattedColorString(customColor, false)
|
||||
} else{
|
||||
color = DialogColorPreference.DISABLE_COLOR
|
||||
summary = ""
|
||||
}
|
||||
} else {
|
||||
dbCustomColorPref?.isEnabled = false
|
||||
// TODO dbGeneralPrefCategory?.removePreference(dbCustomColorPref)
|
||||
}
|
||||
|
||||
// Version
|
||||
@@ -348,12 +339,13 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
}
|
||||
|
||||
private val colorSelectedListener: ((Boolean, Int)-> Unit) = { enable, color ->
|
||||
dbCustomColorPref?.summary = ChromaUtil.getFormattedColorString(color, false)
|
||||
if (enable) {
|
||||
private val colorSelectedListener: ((Int?)-> Unit) = { color ->
|
||||
if (color != null) {
|
||||
dbCustomColorPref?.color = color
|
||||
dbCustomColorPref?.summary = ChromaUtil.getFormattedColorString(color, false)
|
||||
} else {
|
||||
dbCustomColorPref?.color = DialogColorPreference.DISABLE_COLOR
|
||||
dbCustomColorPref?.summary = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +408,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
mDatabase?.defaultUsername = oldDefaultUsername
|
||||
oldDefaultUsername
|
||||
}
|
||||
dbDefaultUsername?.summary = defaultUsernameToShow
|
||||
dbDefaultUsernamePref?.summary = defaultUsernameToShow
|
||||
}
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_COLOR_TASK -> {
|
||||
val oldColor = data.getString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY)!!
|
||||
@@ -426,7 +418,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newColor
|
||||
} else {
|
||||
mDatabase?.customColor = oldColor
|
||||
mDatabase?.customColor = Color.parseColor(oldColor)
|
||||
oldColor
|
||||
}
|
||||
dbCustomColorPref?.summary = defaultColorToShow
|
||||
@@ -632,6 +624,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
|
||||
if (dialogFragment != null && !mDatabaseReadOnly) {
|
||||
@Suppress("DEPRECATION")
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||
}
|
||||
@@ -680,6 +673,27 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
||||
// To reload group when database settings are modified
|
||||
when (preference?.key) {
|
||||
getString(R.string.database_name_key),
|
||||
getString(R.string.database_description_key),
|
||||
getString(R.string.database_default_username_key),
|
||||
getString(R.string.database_custom_color_key),
|
||||
getString(R.string.database_data_compression_key),
|
||||
getString(R.string.database_data_remove_unlinked_attachments_key),
|
||||
getString(R.string.recycle_bin_enable_key),
|
||||
getString(R.string.recycle_bin_group_key),
|
||||
getString(R.string.templates_group_enable_key),
|
||||
getString(R.string.templates_group_uuid_key),
|
||||
getString(R.string.max_history_items_key),
|
||||
getString(R.string.max_history_size_key) -> {
|
||||
NestedAppSettingsFragment.DATABASE_PREFERENCE_CHANGED = true
|
||||
}
|
||||
}
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ open class SettingsActivity
|
||||
|
||||
private var backupManager: BackupManager? = null
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
private var appPropertiesFileCreationRequestCode: Int? = null
|
||||
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
@@ -64,6 +63,41 @@ open class SettingsActivity
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { selectedFileUri ->
|
||||
// Import app properties result
|
||||
try {
|
||||
selectedFileUri?.let { uri ->
|
||||
val appProperties = Properties()
|
||||
contentResolver?.openInputStream(uri)?.use { inputStream ->
|
||||
appProperties.load(inputStream)
|
||||
}
|
||||
PreferencesUtil.setAppProperties(this, appProperties)
|
||||
|
||||
// Restart the current activity
|
||||
reloadActivity()
|
||||
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, "Unable to import app properties", e)
|
||||
}
|
||||
}
|
||||
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||
// Export app properties result
|
||||
try {
|
||||
createdFileUri?.let { uri ->
|
||||
contentResolver?.openOutputStream(uri)?.use { outputStream ->
|
||||
PreferencesUtil
|
||||
.getAppProperties(this)
|
||||
.store(outputStream, getString(R.string.description_app_properties))
|
||||
}
|
||||
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
|
||||
Log.e(DatabaseLockActivity.TAG, "Unable to export app properties", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
|
||||
toolbar?.setTitle(R.string.settings)
|
||||
@@ -217,54 +251,10 @@ open class SettingsActivity
|
||||
}
|
||||
|
||||
fun exportAppProperties() {
|
||||
appPropertiesFileCreationRequestCode = mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name,
|
||||
mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name,
|
||||
DateTime.now().toLocalDateTime().toString("yyyy-MM-dd'_'HH-mm")))
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// Import app properties result
|
||||
try {
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { selectedFileUri ->
|
||||
selectedFileUri?.let { uri ->
|
||||
val appProperties = Properties()
|
||||
contentResolver?.openInputStream(uri)?.use { inputStream ->
|
||||
appProperties.load(inputStream)
|
||||
}
|
||||
PreferencesUtil.setAppProperties(this, appProperties)
|
||||
|
||||
// Restart the current activity
|
||||
reloadActivity()
|
||||
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, "Unable to import app properties", e)
|
||||
}
|
||||
|
||||
// Export app properties result
|
||||
try {
|
||||
if (requestCode == appPropertiesFileCreationRequestCode) {
|
||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||
createdFileUri?.let { uri ->
|
||||
contentResolver?.openOutputStream(uri)?.use { outputStream ->
|
||||
PreferencesUtil
|
||||
.getAppProperties(this)
|
||||
.store(outputStream, getString(R.string.description_app_properties))
|
||||
}
|
||||
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
appPropertiesFileCreationRequestCode = null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
|
||||
Log.e(DatabaseLockActivity.TAG, "Unable to export app properties", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class DialogColorPreference @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
|
||||
override fun getDialogLayoutResource(): Int {
|
||||
return R.layout.pref_dialog_input_color
|
||||
return R.layout.fragment_color_picker
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -30,27 +30,57 @@ import android.view.Window
|
||||
import android.widget.CompoundButton
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.androidclearchroma.IndicatorMode
|
||||
import com.kunzisoft.androidclearchroma.colormode.ColorMode
|
||||
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment
|
||||
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment.*
|
||||
import com.kunzisoft.androidclearchroma.view.ChromaColorView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.ColorPickerDialogFragment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
|
||||
class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
private lateinit var rootView: View
|
||||
private lateinit var enableSwitchView: CompoundButton
|
||||
private var chromaColorFragment: ChromaColorFragment? = null
|
||||
private lateinit var chromaColorView: ChromaColorView
|
||||
|
||||
var onColorSelectedListener: ((enable: Boolean, color: Int) -> Unit)? = null
|
||||
var onColorSelectedListener: ((color: Int?) -> Unit)? = null
|
||||
|
||||
private var mDefaultColor = Color.WHITE
|
||||
private var mActivated = false
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val alertDialogBuilder = AlertDialog.Builder(requireActivity())
|
||||
|
||||
rootView = requireActivity().layoutInflater.inflate(R.layout.pref_dialog_input_color, null)
|
||||
rootView = requireActivity().layoutInflater.inflate(R.layout.fragment_color_picker, null)
|
||||
enableSwitchView = rootView.findViewById(R.id.switch_element)
|
||||
chromaColorView = rootView.findViewById(R.id.chroma_color_view)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
|
||||
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
|
||||
}
|
||||
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
|
||||
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
|
||||
}
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(ARG_INITIAL_COLOR)) {
|
||||
mDefaultColor = getInt(ARG_INITIAL_COLOR)
|
||||
}
|
||||
if (containsKey(ARG_ACTIVATED)) {
|
||||
mActivated = getBoolean(ARG_ACTIVATED)
|
||||
}
|
||||
}
|
||||
}
|
||||
enableSwitchView.isChecked = mActivated
|
||||
chromaColorView.currentColor = mDefaultColor
|
||||
|
||||
chromaColorView.setOnColorChangedListener {
|
||||
if (!enableSwitchView.isChecked)
|
||||
enableSwitchView.isChecked = true
|
||||
}
|
||||
|
||||
alertDialogBuilder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
onDialogClosed(true)
|
||||
@@ -68,8 +98,6 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
|
||||
// request a window without the title
|
||||
dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
|
||||
|
||||
dialog.setOnShowListener { measureLayout(it as Dialog) }
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
@@ -77,73 +105,48 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
database?.let {
|
||||
val initColor = try {
|
||||
var initColor = it.customColor
|
||||
if (initColor != null) {
|
||||
enableSwitchView.isChecked = true
|
||||
Color.parseColor(it.customColor)
|
||||
} catch (e: Exception) {
|
||||
} else {
|
||||
enableSwitchView.isChecked = false
|
||||
DEFAULT_COLOR
|
||||
initColor = DEFAULT_COLOR
|
||||
}
|
||||
chromaColorView.currentColor = initColor
|
||||
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
|
||||
}
|
||||
|
||||
val fragmentManager = childFragmentManager
|
||||
chromaColorFragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_COLORS) as ChromaColorFragment?
|
||||
|
||||
if (chromaColorFragment == null) {
|
||||
chromaColorFragment = newInstance(arguments)
|
||||
fragmentManager.beginTransaction().apply {
|
||||
add(com.kunzisoft.androidclearchroma.R.id.color_dialog_container, chromaColorFragment!!, TAG_FRAGMENT_COLORS)
|
||||
commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(database: Database?, positiveResult: Boolean) {
|
||||
super.onDialogClosed(database, positiveResult)
|
||||
if (positiveResult) {
|
||||
val customColorEnable = enableSwitchView.isChecked
|
||||
chromaColorFragment?.currentColor?.let { currentColor ->
|
||||
onColorSelectedListener?.invoke(customColorEnable, currentColor)
|
||||
database?.let {
|
||||
val newColor = if (customColorEnable) {
|
||||
ChromaUtil.getFormattedColorString(currentColor, false)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val oldColor = database.customColor
|
||||
database.customColor = newColor
|
||||
saveColor(oldColor, newColor)
|
||||
}
|
||||
val newColor: Int? = if (enableSwitchView.isChecked)
|
||||
chromaColorView.currentColor
|
||||
else
|
||||
null
|
||||
onColorSelectedListener?.invoke(newColor)
|
||||
database?.let {
|
||||
val oldColor = database.customColor
|
||||
database.customColor = newColor
|
||||
saveColor(oldColor, newColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new dimensions to dialog
|
||||
* @param ad dialog
|
||||
*/
|
||||
private fun measureLayout(ad: Dialog) {
|
||||
val typedValue = TypedValue()
|
||||
resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_height_multiplier, typedValue, true)
|
||||
val heightMultiplier = typedValue.float
|
||||
val height = (ad.context.resources.displayMetrics.heightPixels * heightMultiplier).toInt()
|
||||
|
||||
resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_width_multiplier, typedValue, true)
|
||||
val widthMultiplier = typedValue.float
|
||||
val width = (ad.context.resources.displayMetrics.widthPixels * widthMultiplier).toInt()
|
||||
|
||||
ad.window?.setLayout(width, height)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
return rootView
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG_FRAGMENT_COLORS = "TAG_FRAGMENT_COLORS"
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
|
||||
outState.putBoolean(ARG_ACTIVATED, mActivated)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
|
||||
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
|
||||
@ColorInt
|
||||
const val DEFAULT_COLOR: Int = Color.WHITE
|
||||
|
||||
@@ -151,9 +154,7 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
|
||||
val fragment = DatabaseColorPreferenceDialogFragmentCompat()
|
||||
val bundle = Bundle(1)
|
||||
bundle.putString(ARG_KEY, key)
|
||||
bundle.putInt(ARG_INITIAL_COLOR, Color.BLACK)
|
||||
bundle.putInt(ARG_COLOR_MODE, ColorMode.RGB.ordinal)
|
||||
bundle.putInt(ARG_INDICATOR_MODE, IndicatorMode.HEX.ordinal)
|
||||
bundle.putInt(ARG_INITIAL_COLOR, DEFAULT_COLOR)
|
||||
fragment.arguments = bundle
|
||||
|
||||
return fragment
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
@@ -76,9 +77,17 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
|
||||
// To inherit to save element in database
|
||||
}
|
||||
|
||||
protected fun saveColor(oldColor: String,
|
||||
newColor: String) {
|
||||
mDatabaseViewModel.saveColor(oldColor, newColor, mDatabaseAutoSaveEnable)
|
||||
protected fun saveColor(oldColor: Int?,
|
||||
newColor: Int?) {
|
||||
val oldColorString = if (oldColor != null)
|
||||
ChromaUtil.getFormattedColorString(oldColor, false)
|
||||
else
|
||||
""
|
||||
val newColorString = if (newColor != null)
|
||||
ChromaUtil.getFormattedColorString(newColor, false)
|
||||
else
|
||||
""
|
||||
mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable)
|
||||
}
|
||||
|
||||
protected fun saveCompression(oldCompression: CompressionAlgorithm,
|
||||
|
||||
@@ -19,7 +19,13 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.NumberPicker
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -62,6 +68,30 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
(context?.applicationContext?.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
setExplanationText(R.string.warning_exact_alarm)
|
||||
setExplanationButton(R.string.permission) {
|
||||
// Open the exact alarm permission screen
|
||||
try {
|
||||
startActivity(Intent().apply {
|
||||
action = ACTION_REQUEST_SCHEDULE_EXACT_ALARM
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open exact alarm permission screen", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
explanationText = ""
|
||||
setExplanationButton("") {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun durationToDaysHoursMinutesSeconds(duration: Long) {
|
||||
if (duration < 0) {
|
||||
mEnabled = false
|
||||
@@ -164,6 +194,7 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DurationDialogFrgCmpt"
|
||||
private const val ENABLE_KEY = "ENABLE_KEY"
|
||||
private const val DAYS_KEY = "DAYS_KEY"
|
||||
private const val HOURS_KEY = "HOURS_KEY"
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
@@ -35,6 +36,7 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
||||
private var inputTextView: EditText? = null
|
||||
private var textUnitView: TextView? = null
|
||||
private var textExplanationView: TextView? = null
|
||||
private var explanationButton: Button? = null
|
||||
private var switchElementView: CompoundButton? = null
|
||||
|
||||
private var mOnInputTextEditorActionListener: TextView.OnEditorActionListener? = null
|
||||
@@ -100,6 +102,27 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
||||
explanationText = getString(explanationTextId)
|
||||
}
|
||||
|
||||
val explanationButtonText: String?
|
||||
get() = explanationButton?.text?.toString() ?: ""
|
||||
|
||||
fun setExplanationButton(explanationButtonText: String?, clickListener: View.OnClickListener) {
|
||||
explanationButton?.apply {
|
||||
if (explanationButtonText != null && explanationButtonText.isNotEmpty()) {
|
||||
text = explanationButtonText
|
||||
visibility = View.VISIBLE
|
||||
setOnClickListener(clickListener)
|
||||
} else {
|
||||
text = ""
|
||||
visibility = View.GONE
|
||||
setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setExplanationButton(@StringRes explanationButtonTextId: Int, clickListener: View.OnClickListener) {
|
||||
setExplanationButton(getString(explanationButtonTextId), clickListener)
|
||||
}
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
@@ -128,6 +151,8 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
||||
textUnitView?.visibility = View.GONE
|
||||
textExplanationView = view.findViewById(R.id.explanation_text)
|
||||
textExplanationView?.visibility = View.GONE
|
||||
explanationButton = view.findViewById(R.id.explanation_button)
|
||||
explanationButton?.visibility = View.GONE
|
||||
switchElementView = view.findViewById(R.id.switch_element)
|
||||
switchElementView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
@@ -31,9 +31,13 @@ abstract class ActionRunnable: Runnable {
|
||||
var result: Result = Result()
|
||||
|
||||
override fun run() {
|
||||
onStartRun()
|
||||
onActionRun()
|
||||
onFinishRun()
|
||||
try {
|
||||
onStartRun()
|
||||
onActionRun()
|
||||
onFinishRun()
|
||||
} catch (runException: Exception) {
|
||||
setError(runException)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onStartRun()
|
||||
|
||||
@@ -43,9 +43,14 @@ object TimeoutHelper {
|
||||
|
||||
private fun getLockPendingIntent(context: Context): PendingIntent {
|
||||
return PendingIntent.getBroadcast(context.applicationContext,
|
||||
REQUEST_ID,
|
||||
Intent(LOCK_ACTION),
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
REQUEST_ID,
|
||||
Intent(LOCK_ACTION),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,9 +66,26 @@ object TimeoutHelper {
|
||||
val triggerTime = System.currentTimeMillis() + timeout
|
||||
Log.d(TAG, "TimeoutHelper start")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
alarmManager.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
alarmManager.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,18 +60,41 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
|
||||
mLockPendingIntent = PendingIntent.getBroadcast(context,
|
||||
4575,
|
||||
Intent(intent).apply {
|
||||
action = LOCK_ACTION
|
||||
},
|
||||
0)
|
||||
4575,
|
||||
Intent(intent).apply {
|
||||
action = LOCK_ACTION
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
// Launch the effective action after a small time
|
||||
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
|
||||
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
alarmManager?.setExact(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent)
|
||||
} else {
|
||||
alarmManager?.set(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent)
|
||||
(context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
mLockPendingIntent
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
mLockPendingIntent
|
||||
)
|
||||
}
|
||||
} else {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
mLockPendingIntent
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cancelLockPendingIntent(context)
|
||||
|
||||
@@ -86,7 +86,7 @@ object UriUtil {
|
||||
|
||||
private fun isFileScheme(fileUri: Uri): Boolean {
|
||||
val scheme = fileUri.scheme
|
||||
if (scheme == null || scheme.isEmpty() || scheme.toLowerCase(Locale.ENGLISH) == "file") {
|
||||
if (scheme == null || scheme.isEmpty() || scheme.lowercase(Locale.ENGLISH) == "file") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -94,7 +94,7 @@ object UriUtil {
|
||||
|
||||
private fun isContentScheme(fileUri: Uri): Boolean {
|
||||
val scheme = fileUri.scheme
|
||||
if (scheme != null && scheme.toLowerCase(Locale.ENGLISH) == "content") {
|
||||
if (scheme != null && scheme.lowercase(Locale.ENGLISH) == "content") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -104,10 +104,12 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
||||
return unlockMessageTextView?.text?.toString() ?: ""
|
||||
}
|
||||
set(value) {
|
||||
if (value == null || value.isEmpty())
|
||||
if (value == null || value.isEmpty()) {
|
||||
unlockMessageTextView?.visibility = GONE
|
||||
else
|
||||
} else {
|
||||
unlockMessageTextView?.visibility = VISIBLE
|
||||
stopIconViewAnimation()
|
||||
}
|
||||
unlockMessageTextView?.text = value ?: ""
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.TextView
|
||||
@@ -9,6 +10,9 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable.Creator
|
||||
|
||||
|
||||
class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@@ -54,4 +58,45 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
||||
UriUtil.getFileData(context, value)?.name ?: value.path
|
||||
} ?: ""
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superState = super.onSaveInstanceState()
|
||||
val saveState = SavedState(superState)
|
||||
saveState.mUri = this.mUri
|
||||
return saveState
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
if (state !is SavedState) {
|
||||
super.onRestoreInstanceState(state)
|
||||
return
|
||||
}
|
||||
super.onRestoreInstanceState(state.superState)
|
||||
this.mUri = state.mUri
|
||||
}
|
||||
|
||||
internal class SavedState : BaseSavedState {
|
||||
var mUri: Uri? = null
|
||||
|
||||
constructor(superState: Parcelable?) : super(superState) {}
|
||||
|
||||
private constructor(parcel: Parcel) : super(parcel) {
|
||||
mUri = parcel.readParcelable(Uri::class.java.classLoader)
|
||||
}
|
||||
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
super.writeToParcel(out, flags)
|
||||
out.writeParcelable(mUri, flags)
|
||||
}
|
||||
|
||||
companion object CREATOR : Creator<SavedState> {
|
||||
override fun createFromParcel(parcel: Parcel): SavedState {
|
||||
return SavedState(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SavedState?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,10 @@ abstract class TemplateAbstractView<
|
||||
|
||||
protected var headerContainerView: ViewGroup
|
||||
protected var entryIconView: ImageView
|
||||
protected var backgroundColorView: View
|
||||
protected var foregroundColorView: View
|
||||
protected var backgroundColorButton: ImageView
|
||||
protected var foregroundColorButton: ImageView
|
||||
private var titleContainerView: ViewGroup
|
||||
protected var templateContainerView: ViewGroup
|
||||
private var customFieldsContainerView: SectionView
|
||||
@@ -57,6 +61,10 @@ abstract class TemplateAbstractView<
|
||||
|
||||
headerContainerView = findViewById(R.id.template_header_container)
|
||||
entryIconView = findViewById(R.id.template_icon_button)
|
||||
backgroundColorView = findViewById(R.id.template_background_color)
|
||||
foregroundColorView = findViewById(R.id.template_foreground_color)
|
||||
backgroundColorButton = findViewById(R.id.template_background_color_button)
|
||||
foregroundColorButton = findViewById(R.id.template_foreground_color_button)
|
||||
titleContainerView = findViewById(R.id.template_title_container)
|
||||
templateContainerView = findViewById(R.id.template_fields_container)
|
||||
// To fix card view margin below Marshmallow
|
||||
@@ -86,7 +94,8 @@ abstract class TemplateAbstractView<
|
||||
if (mTemplate != template) {
|
||||
mTemplate = template
|
||||
if (mEntryInfo != null) {
|
||||
populateEntryInfoWithViews(true)
|
||||
populateEntryInfoWithViews(templateFieldNotEmpty = true,
|
||||
retrieveDefaultValues = false)
|
||||
}
|
||||
buildTemplateAndPopulateInfo()
|
||||
clearFocus()
|
||||
@@ -203,9 +212,7 @@ abstract class TemplateAbstractView<
|
||||
setNumberLines(20)
|
||||
},
|
||||
TemplateAttributeAction.CUSTOM_EDITION
|
||||
).apply {
|
||||
default = field.protectedValue.stringValue
|
||||
}
|
||||
)
|
||||
return buildViewForTemplateField(customFieldTemplateAttribute, field, FIELD_CUSTOM_TAG)
|
||||
}
|
||||
|
||||
@@ -390,50 +397,76 @@ abstract class TemplateAbstractView<
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
protected open fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean) {
|
||||
protected open fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
|
||||
retrieveDefaultValues: Boolean) {
|
||||
if (mEntryInfo == null)
|
||||
mEntryInfo = EntryInfo()
|
||||
|
||||
// Icon already populate
|
||||
|
||||
val titleView: TEntryFieldView? = findViewWithTag(FIELD_TITLE_TAG)
|
||||
titleView?.value?.let {
|
||||
mEntryInfo?.title = it
|
||||
try {
|
||||
val titleView: TEntryFieldView? = findViewWithTag(FIELD_TITLE_TAG)
|
||||
titleView?.value?.let {
|
||||
mEntryInfo?.title = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to populate title view", e)
|
||||
}
|
||||
|
||||
val userNameView: TEntryFieldView? = findViewWithTag(FIELD_USERNAME_TAG)
|
||||
userNameView?.value?.let {
|
||||
mEntryInfo?.username = it
|
||||
try {
|
||||
val userNameView: TEntryFieldView? = findViewWithTag(FIELD_USERNAME_TAG)
|
||||
userNameView?.value?.let {
|
||||
mEntryInfo?.username = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to populate username view", e)
|
||||
}
|
||||
|
||||
val passwordView: TEntryFieldView? = findViewWithTag(FIELD_PASSWORD_TAG)
|
||||
passwordView?.value?.let {
|
||||
mEntryInfo?.password = it
|
||||
try {
|
||||
val passwordView: TEntryFieldView? = findViewWithTag(FIELD_PASSWORD_TAG)
|
||||
passwordView?.value?.let {
|
||||
mEntryInfo?.password = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to populate password view", e)
|
||||
}
|
||||
|
||||
val urlView: TEntryFieldView? = findViewWithTag(FIELD_URL_TAG)
|
||||
urlView?.value?.let {
|
||||
mEntryInfo?.url = it
|
||||
try {
|
||||
val urlView: TEntryFieldView? = findViewWithTag(FIELD_URL_TAG)
|
||||
urlView?.value?.let {
|
||||
mEntryInfo?.url = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to populate url view", e)
|
||||
}
|
||||
|
||||
val expirationView: TDateTimeView? = findViewWithTag(FIELD_EXPIRES_TAG)
|
||||
expirationView?.activation?.let {
|
||||
mEntryInfo?.expires = it
|
||||
}
|
||||
expirationView?.dateTime?.let {
|
||||
mEntryInfo?.expiryTime = it
|
||||
try {
|
||||
val expirationView: TDateTimeView? = findViewWithTag(FIELD_EXPIRES_TAG)
|
||||
expirationView?.activation?.let {
|
||||
mEntryInfo?.expires = it
|
||||
}
|
||||
expirationView?.dateTime?.let {
|
||||
mEntryInfo?.expiryTime = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to populate expiration view", e)
|
||||
}
|
||||
|
||||
val notesView: TEntryFieldView? = findViewWithTag(FIELD_NOTES_TAG)
|
||||
notesView?.value?.let {
|
||||
mEntryInfo?.notes = it
|
||||
try {
|
||||
val notesView: TEntryFieldView? = findViewWithTag(FIELD_NOTES_TAG)
|
||||
notesView?.value?.let {
|
||||
mEntryInfo?.notes = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to populate notes view", e)
|
||||
}
|
||||
|
||||
retrieveCustomFieldsFromView(templateFieldNotEmpty)
|
||||
retrieveCustomFieldsFromView(templateFieldNotEmpty, retrieveDefaultValues)
|
||||
}
|
||||
|
||||
fun getEntryInfo(): EntryInfo {
|
||||
populateEntryInfoWithViews(true)
|
||||
populateEntryInfoWithViews(templateFieldNotEmpty = true,
|
||||
retrieveDefaultValues = true)
|
||||
return mEntryInfo ?: EntryInfo()
|
||||
}
|
||||
|
||||
@@ -479,23 +512,31 @@ abstract class TemplateAbstractView<
|
||||
return mViewFields.indexOfFirst { it.field.name.equals(name, true) }
|
||||
}
|
||||
|
||||
private fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false) {
|
||||
private fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false,
|
||||
retrieveDefaultValues: Boolean = false) {
|
||||
mEntryInfo?.customFields = mViewFields.mapNotNull {
|
||||
getCustomField(it.field.name, templateFieldNotEmpty)
|
||||
getCustomField(it.field.name, templateFieldNotEmpty, retrieveDefaultValues)
|
||||
}.toMutableList()
|
||||
}
|
||||
|
||||
protected fun getCustomField(fieldName: String): Field {
|
||||
return getCustomField(fieldName, false)
|
||||
?: Field(fieldName, ProtectedString(false))
|
||||
return getCustomField(fieldName,
|
||||
templateFieldNotEmpty = false,
|
||||
retrieveDefaultValues = false
|
||||
) ?: Field(fieldName, ProtectedString(false))
|
||||
}
|
||||
|
||||
private fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field? {
|
||||
private fun getCustomField(fieldName: String,
|
||||
templateFieldNotEmpty: Boolean,
|
||||
retrieveDefaultValues: Boolean): Field? {
|
||||
getViewFieldByName(fieldName)?.let { fieldId ->
|
||||
val editView: View? = fieldId.view
|
||||
val editView: View = fieldId.view
|
||||
if (editView is GenericFieldView) {
|
||||
// Do not return field with a default value
|
||||
val defaultViewValue = if (editView.value == editView.default) "" else editView.value
|
||||
val defaultViewValue =
|
||||
if (retrieveDefaultValues || editView.value != editView.default) {
|
||||
editView.value
|
||||
} else ""
|
||||
if (!templateFieldNotEmpty
|
||||
|| (editView.tag == FIELD_CUSTOM_TAG && defaultViewValue.isNotEmpty())) {
|
||||
return Field(
|
||||
@@ -641,7 +682,8 @@ abstract class TemplateAbstractView<
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superSave = super.onSaveInstanceState()
|
||||
val saveState = SavedState(superSave)
|
||||
populateEntryInfoWithViews(false)
|
||||
populateEntryInfoWithViews(templateFieldNotEmpty = false,
|
||||
retrieveDefaultValues = false)
|
||||
saveState.template = this.mTemplate
|
||||
saveState.entryInfo = this.mEntryInfo
|
||||
onSaveEntryInstanceState(saveState)
|
||||
|
||||
@@ -5,13 +5,17 @@ import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.element.template.*
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import org.joda.time.DateTime
|
||||
|
||||
@@ -51,7 +55,53 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||
|
||||
fun setIcon(iconImage: IconImage) {
|
||||
mEntryInfo?.icon = iconImage
|
||||
populateIconMethod?.invoke(entryIconView, iconImage)
|
||||
refreshIcon()
|
||||
}
|
||||
|
||||
fun setOnBackgroundColorClickListener(onClickListener: OnClickListener) {
|
||||
backgroundColorButton.setOnClickListener(onClickListener)
|
||||
}
|
||||
|
||||
fun getBackgroundColor(): Int? {
|
||||
return mEntryInfo?.backgroundColor
|
||||
}
|
||||
|
||||
fun setBackgroundColor(color: Int?) {
|
||||
applyBackgroundColor(color)
|
||||
mEntryInfo?.backgroundColor = color
|
||||
}
|
||||
|
||||
private fun applyBackgroundColor(color: Int?) {
|
||||
if (color != null) {
|
||||
backgroundColorView.background.colorFilter = BlendModeColorFilterCompat
|
||||
.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP)
|
||||
backgroundColorView.visibility = View.VISIBLE
|
||||
} else {
|
||||
backgroundColorView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnForegroundColorClickListener(onClickListener: OnClickListener) {
|
||||
foregroundColorButton.setOnClickListener(onClickListener)
|
||||
}
|
||||
|
||||
fun getForegroundColor(): Int? {
|
||||
return mEntryInfo?.foregroundColor
|
||||
}
|
||||
|
||||
fun setForegroundColor(color: Int?) {
|
||||
applyForegroundColor(color)
|
||||
mEntryInfo?.foregroundColor = color
|
||||
}
|
||||
|
||||
private fun applyForegroundColor(color: Int?) {
|
||||
if (color != null) {
|
||||
foregroundColorView.background.colorFilter = BlendModeColorFilterCompat
|
||||
.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP)
|
||||
foregroundColorView.visibility = View.VISIBLE
|
||||
} else {
|
||||
foregroundColorView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun preProcessTemplate() {
|
||||
@@ -64,6 +114,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||
TextEditFieldView(it).apply {
|
||||
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
|
||||
setProtection(field.protectedValue.isProtected)
|
||||
default = templateAttribute.default
|
||||
setMaxChars(templateAttribute.options.getNumberChars())
|
||||
setMaxLines(templateAttribute.options.getNumberLines())
|
||||
setActionClick(templateAttribute, field, this)
|
||||
@@ -79,7 +130,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||
return context?.let {
|
||||
TextSelectFieldView(it).apply {
|
||||
setItems(templateAttribute.options.getListItems())
|
||||
default = field.protectedValue.stringValue
|
||||
default = templateAttribute.default
|
||||
setActionClick(templateAttribute, field, this)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
|
||||
@@ -195,11 +246,14 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||
|
||||
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<ViewField> {
|
||||
refreshIcon()
|
||||
applyBackgroundColor(mEntryInfo?.backgroundColor)
|
||||
applyForegroundColor(mEntryInfo?.foregroundColor)
|
||||
return super.populateViewsWithEntryInfo(showEmptyFields)
|
||||
}
|
||||
|
||||
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean) {
|
||||
super.populateEntryInfoWithViews(templateFieldNotEmpty)
|
||||
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
|
||||
retrieveDefaultValues: Boolean) {
|
||||
super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues)
|
||||
mEntryInfo?.otpModel = OtpEntryFields.parseFields { key ->
|
||||
getCustomField(key).protectedValue.toString()
|
||||
}?.otpModel
|
||||
|
||||
@@ -73,7 +73,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
private val valueView = AppCompatTextView(context).apply {
|
||||
setTextAppearance(context,
|
||||
R.style.KeepassDXStyle_TextAppearance_TextEntryItem)
|
||||
R.style.KeepassDXStyle_TextAppearance_TextNode)
|
||||
layoutParams = LayoutParams(
|
||||
LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.WRAP_CONTENT).also {
|
||||
|
||||
@@ -194,6 +194,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
|
||||
get() = valueSpinnerAdapter.getItem(mDefaultPosition)
|
||||
set(value) {
|
||||
mDefaultPosition = valueSpinnerAdapter.getPosition(value)
|
||||
valueSpinnerAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun setOnActionClickListener(onActionClickListener: OnClickListener?,
|
||||
|
||||
@@ -80,7 +80,8 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
|
||||
mActionModeCallback = null
|
||||
}
|
||||
|
||||
fun invalidateMenu() {
|
||||
override fun invalidateMenu() {
|
||||
super.invalidateMenu()
|
||||
open()
|
||||
mActionModeCallback?.onPrepareActionMode(actionMode, menu)
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.*
|
||||
import android.text.Selection
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
@@ -37,6 +35,7 @@ import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
@@ -44,6 +43,16 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import androidx.appcompat.view.menu.ActionMenuItemView
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.widget.ActionMenuView
|
||||
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
|
||||
|
||||
/**
|
||||
* Replace font by monospace, must be called after setText()
|
||||
@@ -207,4 +216,50 @@ fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) {
|
||||
Snackbar.make(this, message, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Toolbar.changeControlColor(color: Int) {
|
||||
val colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
||||
for (i in 0 until childCount) {
|
||||
val view: View = getChildAt(i)
|
||||
// Change the color of back button (or open drawer button).
|
||||
if (view is ImageView) {
|
||||
//Action Bar back button
|
||||
view.drawable.colorFilter = colorFilter
|
||||
}
|
||||
if (view is ActionMenuView) {
|
||||
view.post {
|
||||
for (j in 0 until view.childCount) {
|
||||
// Change the color of any ActionMenuViews - icons that
|
||||
// are not back button, nor text, nor overflow menu icon.
|
||||
val innerView: View = view.getChildAt(j)
|
||||
if (innerView is ActionMenuItemView) {
|
||||
innerView.compoundDrawables.forEach { drawable ->
|
||||
//Important to set the color filter in separate thread,
|
||||
//by adding it to the message queue
|
||||
//Won't work otherwise.
|
||||
drawable?.colorFilter = colorFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Change the color of title and subtitle.
|
||||
setTitleTextColor(color)
|
||||
setSubtitleTextColor(color)
|
||||
// Change the color of the Overflow Menu icon.
|
||||
var drawable: Drawable? = overflowIcon
|
||||
if (drawable != null) {
|
||||
drawable = DrawableCompat.wrap(drawable)
|
||||
DrawableCompat.setTint(drawable.mutate(), color)
|
||||
overflowIcon = drawable
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
|
||||
setCollapsedTitleTextColor(color)
|
||||
setExpandedTitleColor(color)
|
||||
invalidate()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user