mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
307 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5872376f50 | ||
|
|
075f72d9f6 | ||
|
|
4441ec1b14 | ||
|
|
0735cc1a54 | ||
|
|
dea2ad6904 | ||
|
|
0f0b6b4a8a | ||
|
|
174e562dcb | ||
|
|
f080750545 | ||
|
|
621415fe51 | ||
|
|
428c2818a5 | ||
|
|
5aa1c70999 | ||
|
|
3508f47842 | ||
|
|
62d4993e6d | ||
|
|
631d946dcf | ||
|
|
8945334f37 | ||
|
|
af2df11a56 | ||
|
|
6dc0c42b1e | ||
|
|
0328293746 | ||
|
|
ad406947cf | ||
|
|
6bb4c1171f | ||
|
|
faa39190fc | ||
|
|
7c0b925c96 | ||
|
|
1b8c453fd0 | ||
|
|
8cc8f595bd | ||
|
|
9a5a8ae23a | ||
|
|
02b27e235c | ||
|
|
5874c5b9cb | ||
|
|
fad09b2cd5 | ||
|
|
cfb08afd7d | ||
|
|
16d939c601 | ||
|
|
af072648c1 | ||
|
|
45d2609494 | ||
|
|
f5ea65f18c | ||
|
|
e9fc6bed23 | ||
|
|
ccc8e4664d | ||
|
|
651ef04137 | ||
|
|
063aba333c | ||
|
|
a3a517ff89 | ||
|
|
40da29b681 | ||
|
|
684d81c895 | ||
|
|
c6ddd3b238 | ||
|
|
3186413bee | ||
|
|
aae1c4cf1c | ||
|
|
e64f264f12 | ||
|
|
08cf747f52 | ||
|
|
383437a3c7 | ||
|
|
c7cba3f50b | ||
|
|
7feb499d50 | ||
|
|
2e6c25b651 | ||
|
|
192903e8d7 | ||
|
|
6f72ade4d4 | ||
|
|
7e3fc0fa59 | ||
|
|
4ea896b57c | ||
|
|
073ccb9b52 | ||
|
|
f32c944d31 | ||
|
|
acd1e3bdfc | ||
|
|
774cbdf0fe | ||
|
|
f5fd527590 | ||
|
|
7ac9a7e94a | ||
|
|
5735f7a945 | ||
|
|
f8f423b5c1 | ||
|
|
81ba7f0721 | ||
|
|
e6be8c23fb | ||
|
|
9cc1764a18 | ||
|
|
6a77adc313 | ||
|
|
e532572d5a | ||
|
|
dcae49c5f8 | ||
|
|
7321c01e8c | ||
|
|
a85b9998c3 | ||
|
|
30d2ce43d1 | ||
|
|
d46edfc9b7 | ||
|
|
79d11138e6 | ||
|
|
f5cd019b6c | ||
|
|
42c4de56fd | ||
|
|
44b3c28a2a | ||
|
|
e5184a1568 | ||
|
|
76d60ded4c | ||
|
|
d2e7e925f7 | ||
|
|
6357a30acb | ||
|
|
4b1fdd0e38 | ||
|
|
227fc060b9 | ||
|
|
32e5aba906 | ||
|
|
55013bb220 | ||
|
|
5544b20d7f | ||
|
|
d6c7f9c68b | ||
|
|
8b5004e500 | ||
|
|
d6cf11b87d | ||
|
|
d4c3a3be6b | ||
|
|
e724b188ef | ||
|
|
ebf92b1103 | ||
|
|
42e2a49af6 | ||
|
|
be7cd3275a | ||
|
|
966df11beb | ||
|
|
fad852f00d | ||
|
|
9c3e6eb823 | ||
|
|
8b88f72efc | ||
|
|
e0aab6cfbf | ||
|
|
29a2e60e05 | ||
|
|
12df74b3a7 | ||
|
|
22d943c9e2 | ||
|
|
5839f51f44 | ||
|
|
4ecb8d4483 | ||
|
|
a08035551a | ||
|
|
c2460d7262 | ||
|
|
4776eac07e | ||
|
|
4952d107dd | ||
|
|
b5d6ee9dee | ||
|
|
e7a30c6024 | ||
|
|
6578e52ec5 | ||
|
|
97508beb5c | ||
|
|
e063b0d6fc | ||
|
|
bb65dc0e81 | ||
|
|
09e00ec119 | ||
|
|
985f8fad3b | ||
|
|
a9accc8c42 | ||
|
|
41316d2bd3 | ||
|
|
2c172eb8d3 | ||
|
|
d49827f9f8 | ||
|
|
b2aafda2b1 | ||
|
|
b4c50e0262 | ||
|
|
fbe51c12c1 | ||
|
|
991959416b | ||
|
|
41f0e61f60 | ||
|
|
eab8cd101f | ||
|
|
97765d798c | ||
|
|
a7f76248ac | ||
|
|
8de670fcf2 | ||
|
|
744823cce4 | ||
|
|
d95a3e00aa | ||
|
|
ccc190f7b0 | ||
|
|
3e6cd98cb9 | ||
|
|
0e3b8fdbb6 | ||
|
|
5aa3f79616 | ||
|
|
42427d0690 | ||
|
|
1a7b32e6d1 | ||
|
|
83555bfdc5 | ||
|
|
03990c1dd9 | ||
|
|
b361be5cb0 | ||
|
|
d02f6d1e67 | ||
|
|
1e56c34e2f | ||
|
|
2a2f8dcecd | ||
|
|
b609ed3ad4 | ||
|
|
80521f8ec2 | ||
|
|
3a5df6a893 | ||
|
|
0e60c4f910 | ||
|
|
f5f2d3c883 | ||
|
|
3c830bfaf2 | ||
|
|
98caf9b5bf | ||
|
|
cfbb8fab1b | ||
|
|
3069e5e566 | ||
|
|
ac050a09e8 | ||
|
|
78406ccdbf | ||
|
|
2efea1bb00 | ||
|
|
0157a160f0 | ||
|
|
eb4084a6a4 | ||
|
|
63f5e5416f | ||
|
|
38e0433e8f | ||
|
|
fc5ffc5f62 | ||
|
|
460e3558a9 | ||
|
|
b06f223124 | ||
|
|
aabfb2adfd | ||
|
|
f9c47c9035 | ||
|
|
e9485ebf56 | ||
|
|
fcd7fd2889 | ||
|
|
279f4a347a | ||
|
|
b29fe23403 | ||
|
|
1972a551e9 | ||
|
|
7e2ffd2ec4 | ||
|
|
06793ae13e | ||
|
|
26e961d356 | ||
|
|
da956d3bd5 | ||
|
|
318a72a123 | ||
|
|
5c4b4864d2 | ||
|
|
4ab31fe21a | ||
|
|
c22a213635 | ||
|
|
aa166a0104 | ||
|
|
2a36626731 | ||
|
|
2f2360fd48 | ||
|
|
e466643229 | ||
|
|
a9044c3dc4 | ||
|
|
2d28cc21a0 | ||
|
|
ebb0e7e118 | ||
|
|
8205454858 | ||
|
|
e909112ab8 | ||
|
|
469923855a | ||
|
|
f2aca08886 | ||
|
|
9900f8fecb | ||
|
|
73224887b9 | ||
|
|
e31574015b | ||
|
|
ef26251469 | ||
|
|
798bce2759 | ||
|
|
88fee5f6de | ||
|
|
937695a1e5 | ||
|
|
a43b580d67 | ||
|
|
b8d0bff22b | ||
|
|
c180d38394 | ||
|
|
10f7d955ff | ||
|
|
143651099a | ||
|
|
63bb12269f | ||
|
|
00ee80184e | ||
|
|
60ba058515 | ||
|
|
1cf8131b6c | ||
|
|
1b38bd59ef | ||
|
|
2e409c3246 | ||
|
|
63fbca8029 | ||
|
|
b37966f79c | ||
|
|
bac5b0de5b | ||
|
|
5dac161553 | ||
|
|
528c167a88 | ||
|
|
cfe01aa996 | ||
|
|
0f53c975cc | ||
|
|
b7b99c77c8 | ||
|
|
1eea5412a5 | ||
|
|
4240465930 | ||
|
|
3dfbf7d2ad | ||
|
|
440490a4bb | ||
|
|
746382811b | ||
|
|
967a54dd50 | ||
|
|
289d9a2531 | ||
|
|
c56b4964fe | ||
|
|
79dbb942f9 | ||
|
|
a5bb5635d3 | ||
|
|
3efe43c0fe | ||
|
|
5fb5299d34 | ||
|
|
b988882251 | ||
|
|
85e82e3fb9 | ||
|
|
35cfe261d2 | ||
|
|
fe4faf9ebc | ||
|
|
751392d656 | ||
|
|
2e5ce5e94f | ||
|
|
535eeb2594 | ||
|
|
8f5e0e93ee | ||
|
|
6f17c5dcac | ||
|
|
4e7c7ba8ce | ||
|
|
6bd5b2345c | ||
|
|
63832ef8fd | ||
|
|
3719bf3593 | ||
|
|
7bca41ca72 | ||
|
|
3f6a9c3af5 | ||
|
|
309380bdd5 | ||
|
|
f135bdb905 | ||
|
|
2b926fd157 | ||
|
|
e911eea69c | ||
|
|
9d182b8299 | ||
|
|
61366e000f | ||
|
|
21cf49f4f8 | ||
|
|
30f0de83d3 | ||
|
|
42278a4b66 | ||
|
|
8752f92cea | ||
|
|
064c468e62 | ||
|
|
ef78fb749c | ||
|
|
a5d1db392b | ||
|
|
6234fc2ca3 | ||
|
|
c9cf90cdc9 | ||
|
|
5b033975b6 | ||
|
|
5663a153f7 | ||
|
|
4b73c45e65 | ||
|
|
ac248d8b73 | ||
|
|
726b0d0fa3 | ||
|
|
2d40164549 | ||
|
|
5203152f78 | ||
|
|
b064bb74cd | ||
|
|
843d8e8e77 | ||
|
|
c5f95b243d | ||
|
|
06f1f4c8ad | ||
|
|
9847f834c2 | ||
|
|
b744d58e6c | ||
|
|
c95358b344 | ||
|
|
2f15d6c9f2 | ||
|
|
d13aa047d5 | ||
|
|
7590d18c67 | ||
|
|
a689116c97 | ||
|
|
3bf7459f05 | ||
|
|
c70faaedd1 | ||
|
|
cb2417fbe4 | ||
|
|
65dd996f2e | ||
|
|
01790a6f31 | ||
|
|
8b84bb893d | ||
|
|
8cb99847c5 | ||
|
|
b3e01277d4 | ||
|
|
b10d407659 | ||
|
|
b7c5a5d238 | ||
|
|
90d1ce63e8 | ||
|
|
9e30b4e5f7 | ||
|
|
e541a8c629 | ||
|
|
0afe25c922 | ||
|
|
bca133430f | ||
|
|
9c925518a7 | ||
|
|
18e79b99e7 | ||
|
|
4e02846df9 | ||
|
|
2268b78bba | ||
|
|
ea8acd0677 | ||
|
|
9e931dd03f | ||
|
|
19e3aabca4 | ||
|
|
ae697d82d5 | ||
|
|
33382273c3 | ||
|
|
7d8466d77a | ||
|
|
5ca08a00d2 | ||
|
|
df3942697e | ||
|
|
5700ca5bcf | ||
|
|
54b2419d64 | ||
|
|
d8b1c94b78 | ||
|
|
2d1ffc23b9 | ||
|
|
da761614bd | ||
|
|
9eb66face5 | ||
|
|
45da17adb8 | ||
|
|
58d10672ea |
18
CHANGELOG
18
CHANGELOG
@@ -1,3 +1,21 @@
|
|||||||
|
KeePassDX(2.9.14)
|
||||||
|
* Add custom icons #96
|
||||||
|
* Dark Themes #532 #714
|
||||||
|
* Fix binary deduplication #715
|
||||||
|
* Fix IconId #901
|
||||||
|
* Resize image stream dynamically to prevent slowdown #919
|
||||||
|
* Small changes #795 #900 #903 #909 #914
|
||||||
|
|
||||||
|
KeePassDX(2.9.13)
|
||||||
|
* Binary image viewer #473 #749
|
||||||
|
* Fix TOTP plugin settings #878
|
||||||
|
* Allow Emoji #796
|
||||||
|
* Scroll and better UI in entry edition screen #876
|
||||||
|
* Better UI #876
|
||||||
|
* Fix themes and add Purple Dark #889
|
||||||
|
* Allow OTP with many padding #585
|
||||||
|
* Add notes in groups #734
|
||||||
|
|
||||||
KeePassDX(2.9.12)
|
KeePassDX(2.9.12)
|
||||||
* Fix OTP token type #863
|
* Fix OTP token type #863
|
||||||
* Fix auto open biometric prompt #862
|
* Fix auto open biometric prompt #862
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
buildToolsVersion '30.0.3'
|
buildToolsVersion "30.0.3"
|
||||||
ndkVersion '21.3.6528147'
|
ndkVersion "21.4.7075529"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode = 56
|
versionCode = 65
|
||||||
versionName = "2.9.12"
|
versionName = "2.9.14"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
|
||||||
@@ -51,7 +50,11 @@ android {
|
|||||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
buildConfigField "boolean", "CLOSED_STORE", "false"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
|
"{\"KeepassDXStyle_Red\"," +
|
||||||
|
"\"KeepassDXStyle_Red_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
|
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
}
|
}
|
||||||
pro {
|
pro {
|
||||||
@@ -70,7 +73,13 @@ android {
|
|||||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
|
"{\"KeepassDXStyle_Blue\"," +
|
||||||
|
"\"KeepassDXStyle_Blue_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Red\"," +
|
||||||
|
"\"KeepassDXStyle_Red_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
|
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||||
}
|
}
|
||||||
@@ -82,6 +91,10 @@ android {
|
|||||||
free.res.srcDir 'src/free/res'
|
free.res.srcDir 'src/free/res'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests.includeAndroidResources = true
|
||||||
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@@ -100,18 +113,19 @@ dependencies {
|
|||||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.1.0-rc01'
|
implementation 'androidx.biometric:biometric:1.1.0-rc01'
|
||||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
// WARNING: To upgrade with style, bug in edit text
|
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
// Database
|
// Database
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
// Autofill
|
// Autofill
|
||||||
implementation "androidx.autofill:autofill:1.1.0-rc01"
|
implementation "androidx.autofill:autofill:1.1.0"
|
||||||
// Crypto
|
// Crypto
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
||||||
// Time
|
// Time
|
||||||
@@ -120,14 +134,14 @@ dependencies {
|
|||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||||
// Education
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||||
// Apache Commons Collections
|
// Apache Commons
|
||||||
implementation 'commons-collections:commons-collections:3.2.2'
|
implementation 'commons-io:commons-io:2.8.0'
|
||||||
// Apache Commons Codec
|
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
// Icon pack
|
// Icon pack
|
||||||
implementation project(path: ':icon-pack-classic')
|
implementation project(path: ':icon-pack-classic')
|
||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation 'junit:junit:4.13'
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
|
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/androidTest/assets/test_image.png
Normal file
BIN
app/src/androidTest/assets/test_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
133
app/src/androidTest/assets/test_text.txt
Normal file
133
app/src/androidTest/assets/test_text.txt
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
Basic Latin
|
||||||
|
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
|
||||||
|
Latin-1 Supplement
|
||||||
|
¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ® ¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï Ð Ñ Ò Ó Ô Õ Ö × Ø Ù Ú Û Ü Ý Þ ß à á â ã ä å æ ç è é ê ë ì í î ï ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ
|
||||||
|
Latin Extended-A
|
||||||
|
Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď ď Đ đ Ē ē Ĕ ĕ Ė ė Ę ę Ě ě Ĝ ĝ Ğ ğ Ġ ġ Ģ ģ Ĥ ĥ Ħ ħ Ĩ ĩ Ī ī Ĭ ĭ Į į İ ı IJ ij Ĵ ĵ Ķ ķ ĸ Ĺ ĺ Ļ ļ Ľ ľ Ŀ ŀ Ł ł Ń ń Ņ ņ Ň ň ʼn Ŋ ŋ Ō ō Ŏ ŏ Ő ő Œ œ Ŕ ŕ Ŗ ŗ Ř ř Ś ś Ŝ ŝ Ş ş Š š Ţ ţ Ť ť Ŧ ŧ Ũ ũ Ū ū Ŭ ŭ Ů ů Ű ű Ų ų Ŵ ŵ Ŷ ŷ Ÿ Ź ź Ż ż Ž ž ſ
|
||||||
|
Latin Extended-B
|
||||||
|
ƀ Ɓ Ƃ ƃ Ƅ ƅ Ɔ Ƈ ƈ Ɖ Ɗ Ƌ ƌ ƍ Ǝ Ə Ɛ Ƒ ƒ Ɠ Ɣ ƕ Ɩ Ɨ Ƙ ƙ ƚ ƛ Ɯ Ɲ ƞ Ɵ Ơ ơ Ƣ ƣ Ƥ ƥ Ʀ Ƨ ƨ Ʃ ƪ ƫ Ƭ ƭ Ʈ Ư ư Ʊ Ʋ Ƴ ƴ Ƶ ƶ Ʒ Ƹ ƹ ƺ ƻ Ƽ ƽ ƾ ƿ ǀ ǁ ǂ ǃ DŽ Dž dž LJ Lj lj NJ Nj nj Ǎ ǎ Ǐ ǐ Ǒ ǒ Ǔ ǔ Ǖ ǖ Ǘ ǘ Ǚ ǚ Ǜ ǜ ǝ Ǟ ǟ Ǡ ǡ Ǣ ǣ Ǥ ǥ Ǧ ǧ Ǩ ǩ Ǫ ǫ Ǭ ǭ Ǯ ǯ ǰ DZ Dz dz Ǵ ǵ Ǻ ǻ Ǽ ǽ Ǿ ǿ Ȁ ȁ Ȃ ȃ ...
|
||||||
|
IPA Extensions
|
||||||
|
ɐ ɑ ɒ ɓ ɔ ɕ ɖ ɗ ɘ ə ɚ ɛ ɜ ɝ ɞ ɟ ɠ ɡ ɢ ɣ ɤ ɥ ɦ ɧ ɨ ɩ ɪ ɫ ɬ ɭ ɮ ɯ ɰ ɱ ɲ ɳ ɴ ɵ ɶ ɷ ɸ ɹ ɺ ɻ ɼ ɽ ɾ ɿ ʀ ʁ ʂ ʃ ʄ ʅ ʆ ʇ ʈ ʉ ʊ ʋ ʌ ʍ ʎ ʏ ʐ ʑ ʒ ʓ ʔ ʕ ʖ ʗ ʘ ʙ ʚ ʛ ʜ ʝ ʞ ʟ ʠ ʡ ʢ ʣ ʤ ʥ ʦ ʧ ʨ
|
||||||
|
Spacing Modifier Letters
|
||||||
|
ʰ ʱ ʲ ʳ ʴ ʵ ʶ ʷ ʸ ʹ ʺ ʻ ʼ ʽ ʾ ʿ ˀ ˁ ˂ ˃ ˄ ˅ ˆ ˇ ˈ ˉ ˊ ˋ ˌ ˍ ˎ ˏ ː ˑ ˒ ˓ ˔ ˕ ˖ ˗ ˘ ˙ ˚ ˛ ˜ ˝ ˞ ˠ ˡ ˢ ˣ ˤ ˥ ˦ ˧ ˨ ˩
|
||||||
|
Combining Diacritical Marks
|
||||||
|
̀ ́ ̂ ̃ ̄ ̅ ̆ ̇ ̈ ̉ ̊ ̋ ̌ ̍ ̎ ̏ ̐ ̑ ̒ ̓ ̔ ̕ ̖ ̗ ̘ ̙ ̚ ̛ ̜ ̝ ̞ ̟ ̠ ̡ ̢ ̣ ̤ ̥ ̦ ̧ ̨ ̩ ̪ ̫ ̬ ̭ ̮ ̯ ̰ ̱ ̲ ̳ ̴ ̵ ̶ ̷ ̸ ̹ ̺ ̻ ̼ ̽ ̾ ̿ ̀ ́ ͂ ̓ ̈́ ͅ ͠ ͡
|
||||||
|
Greek
|
||||||
|
ʹ ͵ ͺ ; ΄ ΅ Ά · Έ Ή Ί Ό Ύ Ώ ΐ Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω Ϊ Ϋ ά έ ή ί ΰ α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ ς σ τ υ φ χ ψ ω ϊ ϋ ό ύ ώ ϐ ϑ ϒ ϓ ϔ ϕ ϖ Ϛ Ϝ Ϟ Ϡ Ϣ ϣ Ϥ ϥ Ϧ ϧ Ϩ ϩ Ϫ ϫ Ϭ ϭ Ϯ ϯ ϰ ϱ ϲ ϳ
|
||||||
|
Cyrillic
|
||||||
|
Ё Ђ Ѓ Є Ѕ І Ї Ј Љ Њ Ћ Ќ Ў Џ А Б В Г Д Е Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я а б в г д е ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я ё ђ ѓ є ѕ і ї ј љ њ ћ ќ ў џ Ѡ ѡ Ѣ ѣ Ѥ ѥ Ѧ ѧ Ѩ ѩ Ѫ ѫ Ѭ ѭ Ѯ ѯ Ѱ ѱ Ѳ ѳ Ѵ ѵ Ѷ ѷ Ѹ ѹ Ѻ ѻ Ѽ ѽ Ѿ ѿ Ҁ ҁ ҂ ҃ ...
|
||||||
|
Armenian
|
||||||
|
Ա Բ Գ Դ Ե Զ Է Ը Թ Ժ Ի Լ Խ Ծ Կ Հ Ձ Ղ Ճ Մ Յ Ն Շ Ո Չ Պ Ջ Ռ Ս Վ Տ Ր Ց Ւ Փ Ք Օ Ֆ ՙ ՚ ՛ ՜ ՝ ՞ ՟ ա բ գ դ ե զ է ը թ ժ ի լ խ ծ կ հ ձ ղ ճ մ յ ն շ ո չ պ ջ ռ ս վ տ ր ց ւ փ ք օ ֆ և ։
|
||||||
|
Hebrew
|
||||||
|
֑ ֒ ֓ ֔ ֕ ֖ ֗ ֘ ֙ ֚ ֛ ֜ ֝ ֞ ֟ ֠ ֡ ֣ ֤ ֥ ֦ ֧ ֨ ֩ ֪ ֫ ֬ ֭ ֮ ֯ ְ ֱ ֲ ֳ ִ ֵ ֶ ַ ָ ֹ ֻ ּ ֽ ־ ֿ ׀ ׁ ׂ ׃ ׄ א ב ג ד ה ו ז ח ט י ך כ ל ם מ ן נ ס ע ף פ ץ צ ק ר ש ת װ ױ ײ ׳ ״
|
||||||
|
Arabic
|
||||||
|
، ؛ ؟ ء آ أ ؤ إ ئ ا ب ة ت ث ج ح خ د ذ ر ز س ش ص ض ط ظ ع غ ـ ف ق ك ل م ن ه و ى ي ً ٌ ٍ َ ُ ِ ّ ْ ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩ ٪ ٫ ٬ ٭ ٰ ٱ ٲ ٳ ٴ ٵ ٶ ٷ ٸ ٹ ٺ ٻ ټ ٽ پ ٿ ڀ ځ ڂ ڃ ڄ څ چ ڇ ڈ ډ ڊ ڋ ڌ ڍ ڎ ڏ ڐ ڑ ڒ ړ ڔ ڕ ږ ڗ ژ ڙ ښ ڛ ڜ ڝ ڞ ڟ ڠ ڡ ڢ ڣ ڤ ڥ ڦ ڧ ڨ ک ڪ ګ ڬ ڭ ڮ گ ڰ ڱ ...
|
||||||
|
Devanagari
|
||||||
|
ँ ं ः अ आ इ ई उ ऊ ऋ ऌ ऍ ऎ ए ऐ ऑ ऒ ओ औ क ख ग घ ङ च छ ज झ ञ ट ठ ड ढ ण त थ द ध न ऩ प फ ब भ म य र ऱ ल ळ ऴ व श ष स ह ़ ऽ ा ि ी ु ू ृ ॄ ॅ ॆ े ै ॉ ॊ ो ौ ् ॐ ॑ ॒ ॓ ॔ क़ ख़ ग़ ज़ ड़ ढ़ फ़ य़ ॠ ॡ ॢ ॣ । ॥ ० १ २ ३ ४ ५ ६ ७ ८ ९ ॰
|
||||||
|
Bengali
|
||||||
|
ঁ ং ঃ অ আ ই ঈ উ ঊ ঋ ঌ এ ঐ ও ঔ ক খ গ ঘ ঙ চ ছ জ ঝ ঞ ট ঠ ড ঢ ণ ত থ দ ধ ন প ফ ব ভ ম য র ল শ ষ স হ ় া ি ী ু ূ ৃ ৄ ে ৈ ো ৌ ্ ৗ ড় ঢ় য় ৠ ৡ ৢ ৣ ০ ১ ২ ৩ ৪ ৫ ৬ ৭ ৮ ৯ ৰ ৱ ৲ ৳ ৴ ৵ ৶ ৷ ৸ ৹ ৺
|
||||||
|
Gurmukhi
|
||||||
|
ਂ ਅ ਆ ਇ ਈ ਉ ਊ ਏ ਐ ਓ ਔ ਕ ਖ ਗ ਘ ਙ ਚ ਛ ਜ ਝ ਞ ਟ ਠ ਡ ਢ ਣ ਤ ਥ ਦ ਧ ਨ ਪ ਫ ਬ ਭ ਮ ਯ ਰ ਲ ਲ਼ ਵ ਸ਼ ਸ ਹ ਼ ਾ ਿ ੀ ੁ ੂ ੇ ੈ ੋ ੌ ੍ ਖ਼ ਗ਼ ਜ਼ ੜ ਫ਼ ੦ ੧ ੨ ੩ ੪ ੫ ੬ ੭ ੮ ੯ ੰ ੱ ੲ ੳ ੴ
|
||||||
|
Gujarati
|
||||||
|
ઁ ં ઃ અ આ ઇ ઈ ઉ ઊ ઋ ઍ એ ઐ ઑ ઓ ઔ ક ખ ગ ઘ ઙ ચ છ જ ઝ ઞ ટ ઠ ડ ઢ ણ ત થ દ ધ ન પ ફ બ ભ મ ય ર લ ળ વ શ ષ સ હ ઼ ઽ ા િ ી ુ ૂ ૃ ૄ ૅ ે ૈ ૉ ો ૌ ્ ૐ ૠ ૦ ૧ ૨ ૩ ૪ ૫ ૬ ૭ ૮ ૯
|
||||||
|
Oriya
|
||||||
|
ଁ ଂ ଃ ଅ ଆ ଇ ଈ ଉ ଊ ଋ ଌ ଏ ଐ ଓ ଔ କ ଖ ଗ ଘ ଙ ଚ ଛ ଜ ଝ ଞ ଟ ଠ ଡ ଢ ଣ ତ ଥ ଦ ଧ ନ ପ ଫ ବ ଭ ମ ଯ ର ଲ ଳ ଶ ଷ ସ ହ ଼ ଽ ା ି ୀ ୁ ୂ ୃ େ ୈ ୋ ୌ ୍ ୖ ୗ ଡ଼ ଢ଼ ୟ ୠ ୡ ୦ ୧ ୨ ୩ ୪ ୫ ୬ ୭ ୮ ୯ ୰
|
||||||
|
Tamil
|
||||||
|
ஂ ஃ அ ஆ இ ஈ உ ஊ எ ஏ ஐ ஒ ஓ ஔ க ங ச ஜ ஞ ட ண த ந ன ப ம ய ர ற ல ள ழ வ ஷ ஸ ஹ ா ி ீ ு ூ ெ ே ை ொ ோ ௌ ் ௗ ௧ ௨ ௩ ௪ ௫ ௬ ௭ ௮ ௯ ௰ ௱ ௲
|
||||||
|
Telugu
|
||||||
|
ఁ ం ః అ ఆ ఇ ఈ ఉ ఊ ఋ ఌ ఎ ఏ ఐ ఒ ఓ ఔ క ఖ గ ఘ ఙ చ ఛ జ ఝ ఞ ట ఠ డ ఢ ణ త థ ద ధ న ప ఫ బ భ మ య ర ఱ ల ళ వ శ ష స హ ా ి ీ ు ూ ృ ౄ ె ే ై ొ ో ౌ ్ ౕ ౖ ౠ ౡ ౦ ౧ ౨ ౩ ౪ ౫ ౬ ౭ ౮ ౯
|
||||||
|
Kannada
|
||||||
|
ಂ ಃ ಅ ಆ ಇ ಈ ಉ ಊ ಋ ಌ ಎ ಏ ಐ ಒ ಓ ಔ ಕ ಖ ಗ ಘ ಙ ಚ ಛ ಜ ಝ ಞ ಟ ಠ ಡ ಢ ಣ ತ ಥ ದ ಧ ನ ಪ ಫ ಬ ಭ ಮ ಯ ರ ಱ ಲ ಳ ವ ಶ ಷ ಸ ಹ ಾ ಿ ೀ ು ೂ ೃ ೄ ೆ ೇ ೈ ೊ ೋ ೌ ್ ೕ ೖ ೞ ೠ ೡ ೦ ೧ ೨ ೩ ೪ ೫ ೬ ೭ ೮ ೯
|
||||||
|
Malayalam
|
||||||
|
ം ഃ അ ആ ഇ ഈ ഉ ഊ ഋ ഌ എ ഏ ഐ ഒ ഓ ഔ ക ഖ ഗ ഘ ങ ച ഛ ജ ഝ ഞ ട ഠ ഡ ഢ ണ ത ഥ ദ ധ ന പ ഫ ബ ഭ മ യ ര റ ല ള ഴ വ ശ ഷ സ ഹ ാ ി ീ ു ൂ ൃ െ േ ൈ ൊ ോ ൌ ് ൗ ൠ ൡ ൦ ൧ ൨ ൩ ൪ ൫ ൬ ൭ ൮ ൯
|
||||||
|
Thai
|
||||||
|
ก ข ฃ ค ฅ ฆ ง จ ฉ ช ซ ฌ ญ ฎ ฏ ฐ ฑ ฒ ณ ด ต ถ ท ธ น บ ป ผ ฝ พ ฟ ภ ม ย ร ฤ ล ฦ ว ศ ษ ส ห ฬ อ ฮ ฯ ะ ั า ำ ิ ี ึ ื ุ ู ฺ ฿ เ แ โ ใ ไ ๅ ๆ ็ ่ ้ ๊ ๋ ์ ํ ๎ ๏ ๐ ๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙ ๚ ๛
|
||||||
|
Lao
|
||||||
|
ກ ຂ ຄ ງ ຈ ຊ ຍ ດ ຕ ຖ ທ ນ ບ ປ ຜ ຝ ພ ຟ ມ ຢ ຣ ລ ວ ສ ຫ ອ ຮ ຯ ະ ັ າ ຳ ິ ີ ຶ ື ຸ ູ ົ ຼ ຽ ເ ແ ໂ ໃ ໄ ໆ ່ ້ ໊ ໋ ໌ ໍ ໐ ໑ ໒ ໓ ໔ ໕ ໖ ໗ ໘ ໙ ໜ ໝ
|
||||||
|
Tibetan
|
||||||
|
ༀ ༁ ༂ ༃ ༄ ༅ ༆ ༇ ༈ ༉ ༊ ་ ༌ ། ༎ ༏ ༐ ༑ ༒ ༓ ༔ ༕ ༖ ༗ ༘ ༙ ༚ ༛ ༜ ༝ ༞ ༟ ༠ ༡ ༢ ༣ ༤ ༥ ༦ ༧ ༨ ༩ ༪ ༫ ༬ ༭ ༮ ༯ ༰ ༱ ༲ ༳ ༴ ༵ ༶ ༷ ༸ ༹ ༺ ༻ ༼ ༽ ༾ ༿ ཀ ཁ ག གྷ ང ཅ ཆ ཇ ཉ ཊ ཋ ཌ ཌྷ ཎ ཏ ཐ ད དྷ ན པ ཕ བ བྷ མ ཙ ཚ ཛ ཛྷ ཝ ཞ ཟ འ ཡ ར ལ ཤ ཥ ས ཧ ཨ ཀྵ ཱ ི ཱི ུ ཱུ ྲྀ ཷ ླྀ ཹ ེ ཻ ོ ཽ ཾ ཿ ྀ ཱྀ ྂ ྃ ྄ ྅ ྆ ྇ ...
|
||||||
|
Georgian
|
||||||
|
Ⴀ Ⴁ Ⴂ Ⴃ Ⴄ Ⴅ Ⴆ Ⴇ Ⴈ Ⴉ Ⴊ Ⴋ Ⴌ Ⴍ Ⴎ Ⴏ Ⴐ Ⴑ Ⴒ Ⴓ Ⴔ Ⴕ Ⴖ Ⴗ Ⴘ Ⴙ Ⴚ Ⴛ Ⴜ Ⴝ Ⴞ Ⴟ Ⴠ Ⴡ Ⴢ Ⴣ Ⴤ Ⴥ ა ბ გ დ ე ვ ზ თ ი კ ლ მ ნ ო პ ჟ რ ს ტ უ ფ ქ ღ ყ შ ჩ ც ძ წ ჭ ხ ჯ ჰ ჱ ჲ ჳ ჴ ჵ ჶ ჻
|
||||||
|
Hangul Jamo
|
||||||
|
ᄀ ᄁ ᄂ ᄃ ᄄ ᄅ ᄆ ᄇ ᄈ ᄉ ᄊ ᄋ ᄌ ᄍ ᄎ ᄏ ᄐ ᄑ ᄒ ᄓ ᄔ ᄕ ᄖ ᄗ ᄘ ᄙ ᄚ ᄛ ᄜ ᄝ ᄞ ᄟ ᄠ ᄡ ᄢ ᄣ ᄤ ᄥ ᄦ ᄧ ᄨ ᄩ ᄪ ᄫ ᄬ ᄭ ᄮ ᄯ ᄰ ᄱ ᄲ ᄳ ᄴ ᄵ ᄶ ᄷ ᄸ ᄹ ᄺ ᄻ ᄼ ᄽ ᄾ ᄿ ᅀ ᅁ ᅂ ᅃ ᅄ ᅅ ᅆ ᅇ ᅈ ᅉ ᅊ ᅋ ᅌ ᅍ ᅎ ᅏ ᅐ ᅑ ᅒ ᅓ ᅔ ᅕ ᅖ ᅗ ᅘ ᅙ ᅟ ᅠ ᅡ ᅢ ᅣ ᅤ ᅥ ᅦ ᅧ ᅨ ᅩ ᅪ ᅫ ᅬ ᅭ ᅮ ᅯ ᅰ ᅱ ᅲ ᅳ ᅴ ᅵ ᅶ ᅷ ᅸ ᅹ ᅺ ᅻ ᅼ ᅽ ᅾ ᅿ ᆀ ᆁ ᆂ ᆃ ᆄ ...
|
||||||
|
Latin Extended Additional
|
||||||
|
Ḁ ḁ Ḃ ḃ Ḅ ḅ Ḇ ḇ Ḉ ḉ Ḋ ḋ Ḍ ḍ Ḏ ḏ Ḑ ḑ Ḓ ḓ Ḕ ḕ Ḗ ḗ Ḙ ḙ Ḛ ḛ Ḝ ḝ Ḟ ḟ Ḡ ḡ Ḣ ḣ Ḥ ḥ Ḧ ḧ Ḩ ḩ Ḫ ḫ Ḭ ḭ Ḯ ḯ Ḱ ḱ Ḳ ḳ Ḵ ḵ Ḷ ḷ Ḹ ḹ Ḻ ḻ Ḽ ḽ Ḿ ḿ Ṁ ṁ Ṃ ṃ Ṅ ṅ Ṇ ṇ Ṉ ṉ Ṋ ṋ Ṍ ṍ Ṏ ṏ Ṑ ṑ Ṓ ṓ Ṕ ṕ Ṗ ṗ Ṙ ṙ Ṛ ṛ Ṝ ṝ Ṟ ṟ Ṡ ṡ Ṣ ṣ Ṥ ṥ Ṧ ṧ Ṩ ṩ Ṫ ṫ Ṭ ṭ Ṯ ṯ Ṱ ṱ Ṳ ṳ Ṵ ṵ Ṷ ṷ Ṹ ṹ Ṻ ṻ Ṽ ṽ Ṿ ṿ ...
|
||||||
|
Greek Extended
|
||||||
|
ἀ ἁ ἂ ἃ ἄ ἅ ἆ ἇ Ἀ Ἁ Ἂ Ἃ Ἄ Ἅ Ἆ Ἇ ἐ ἑ ἒ ἓ ἔ ἕ Ἐ Ἑ Ἒ Ἓ Ἔ Ἕ ἠ ἡ ἢ ἣ ἤ ἥ ἦ ἧ Ἠ Ἡ Ἢ Ἣ Ἤ Ἥ Ἦ Ἧ ἰ ἱ ἲ ἳ ἴ ἵ ἶ ἷ Ἰ Ἱ Ἲ Ἳ Ἴ Ἵ Ἶ Ἷ ὀ ὁ ὂ ὃ ὄ ὅ Ὀ Ὁ Ὂ Ὃ Ὄ Ὅ ὐ ὑ ὒ ὓ ὔ ὕ ὖ ὗ Ὑ Ὓ Ὕ Ὗ ὠ ὡ ὢ ὣ ὤ ὥ ὦ ὧ Ὠ Ὡ Ὢ Ὣ Ὤ Ὥ Ὦ Ὧ ὰ ά ὲ έ ὴ ή ὶ ί ὸ ό ὺ ύ ὼ ώ ᾀ ᾁ ᾂ ᾃ ᾄ ᾅ ᾆ ᾇ ᾈ ᾉ ᾊ ᾋ ᾌ ᾍ ...
|
||||||
|
General Punctuation
|
||||||
|
‐ ‑ ‒ – — ― ‖ ‗ ‘ ’ ‚ ‛ “ ” „ ‟ † ‡ • ‣ ․ ‥ … ‧
‰ ‱ ′ ″ ‴ ‵ ‶ ‷ ‸ ‹ › ※ ‼ ‽ ‾ ‿ ⁀ ⁁ ⁂ ⁃ ⁄ ⁅ ⁆
|
||||||
|
Superscripts and Subscripts
|
||||||
|
⁰ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾ ⁿ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₊ ₋ ₌ ₍ ₎
|
||||||
|
Currency Symbols
|
||||||
|
₠ ₡ ₢ ₣ ₤ ₥ ₦ ₧ ₨ ₩ ₪ ₫
|
||||||
|
Combining Marks for Symbols
|
||||||
|
⃐ ⃑ ⃒ ⃓ ⃔ ⃕ ⃖ ⃗ ⃘ ⃙ ⃚ ⃛ ⃜ ⃝ ⃞ ⃟ ⃠ ⃡
|
||||||
|
Letterlike Symbols
|
||||||
|
℀ ℁ ℂ ℃ ℄ ℅ ℆ ℇ ℈ ℉ ℊ ℋ ℌ ℍ ℎ ℏ ℐ ℑ ℒ ℓ ℔ ℕ № ℗ ℘ ℙ ℚ ℛ ℜ ℝ ℞ ℟ ℠ ℡ ™ ℣ ℤ ℥ Ω ℧ ℨ ℩ K Å ℬ ℭ ℮ ℯ ℰ ℱ Ⅎ ℳ ℴ ℵ ℶ ℷ ℸ
|
||||||
|
Number Forms
|
||||||
|
⅓ ⅔ ⅕ ⅖ ⅗ ⅘ ⅙ ⅚ ⅛ ⅜ ⅝ ⅞ ⅟ Ⅰ Ⅱ Ⅲ Ⅳ Ⅴ Ⅵ Ⅶ Ⅷ Ⅸ Ⅹ Ⅺ Ⅻ Ⅼ Ⅽ Ⅾ Ⅿ ⅰ ⅱ ⅲ ⅳ ⅴ ⅵ ⅶ ⅷ ⅸ ⅹ ⅺ ⅻ ⅼ ⅽ ⅾ ⅿ ↀ ↁ ↂ
|
||||||
|
Arrows
|
||||||
|
← ↑ → ↓ ↔ ↕ ↖ ↗ ↘ ↙ ↚ ↛ ↜ ↝ ↞ ↟ ↠ ↡ ↢ ↣ ↤ ↥ ↦ ↧ ↨ ↩ ↪ ↫ ↬ ↭ ↮ ↯ ↰ ↱ ↲ ↳ ↴ ↵ ↶ ↷ ↸ ↹ ↺ ↻ ↼ ↽ ↾ ↿ ⇀ ⇁ ⇂ ⇃ ⇄ ⇅ ⇆ ⇇ ⇈ ⇉ ⇊ ⇋ ⇌ ⇍ ⇎ ⇏ ⇐ ⇑ ⇒ ⇓ ⇔ ⇕ ⇖ ⇗ ⇘ ⇙ ⇚ ⇛ ⇜ ⇝ ⇞ ⇟ ⇠ ⇡ ⇢ ⇣ ⇤ ⇥ ⇦ ⇧ ⇨ ⇩ ⇪
|
||||||
|
Mathematical Operators
|
||||||
|
∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏ ∐ ∑ − ∓ ∔ ∕ ∖ ∗ ∘ ∙ √ ∛ ∜ ∝ ∞ ∟ ∠ ∡ ∢ ∣ ∤ ∥ ∦ ∧ ∨ ∩ ∪ ∫ ∬ ∭ ∮ ∯ ∰ ∱ ∲ ∳ ∴ ∵ ∶ ∷ ∸ ∹ ∺ ∻ ∼ ∽ ∾ ∿ ≀ ≁ ≂ ≃ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≏ ≐ ≑ ≒ ≓ ≔ ≕ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟ ≠ ≡ ≢ ≣ ≤ ≥ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯ ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿ ...
|
||||||
|
Miscellaneous Technical
|
||||||
|
⌀ ⌂ ⌃ ⌄ ⌅ ⌆ ⌇ ⌈ ⌉ ⌊ ⌋ ⌌ ⌍ ⌎ ⌏ ⌐ ⌑ ⌒ ⌓ ⌔ ⌕ ⌖ ⌗ ⌘ ⌙ ⌚ ⌛ ⌜ ⌝ ⌞ ⌟ ⌠ ⌡ ⌢ ⌣ ⌤ ⌥ ⌦ ⌧ ⌨ 〈 〉 ⌫ ⌬ ⌭ ⌮ ⌯ ⌰ ⌱ ⌲ ⌳ ⌴ ⌵ ⌶ ⌷ ⌸ ⌹ ⌺ ⌻ ⌼ ⌽ ⌾ ⌿ ⍀ ⍁ ⍂ ⍃ ⍄ ⍅ ⍆ ⍇ ⍈ ⍉ ⍊ ⍋ ⍌ ⍍ ⍎ ⍏ ⍐ ⍑ ⍒ ⍓ ⍔ ⍕ ⍖ ⍗ ⍘ ⍙ ⍚ ⍛ ⍜ ⍝ ⍞ ⍟ ⍠ ⍡ ⍢ ⍣ ⍤ ⍥ ⍦ ⍧ ⍨ ⍩ ⍪ ⍫ ⍬ ⍭ ⍮ ⍯ ⍰ ⍱ ⍲ ⍳ ⍴ ⍵ ⍶ ⍷ ⍸ ⍹ ⍺
|
||||||
|
Control Pictures
|
||||||
|
␀ ␁ ␂ ␃ ␄ ␅ ␆ ␇ ␈ ␉ ␊ ␋ ␌ ␍ ␎ ␏ ␐ ␑ ␒ ␓ ␔ ␕ ␖ ␗ ␘ ␙ ␚ ␛ ␜ ␝ ␞ ␟ ␠ ␡ ␢ ␣ 
|
||||||
|
Optical Character Recognition
|
||||||
|
⑀ ⑁ ⑂ ⑃ ⑄ ⑅ ⑆ ⑇ ⑈ ⑉ ⑊
|
||||||
|
Enclosed Alphanumerics
|
||||||
|
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇ ⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ...
|
||||||
|
Box Drawing
|
||||||
|
─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
||||||
|
Block Elements
|
||||||
|
▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕
|
||||||
|
Geometric Shapes
|
||||||
|
■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▪ ▫ ▬ ▭ ▮ ▯ ▰ ▱ ▲ △ ▴ ▵ ▶ ▷ ▸ ▹ ► ▻ ▼ ▽ ▾ ▿ ◀ ◁ ◂ ◃ ◄ ◅ ◆ ◇ ◈ ◉ ◊ ○ ◌ ◍ ◎ ● ◐ ◑ ◒ ◓ ◔ ◕ ◖ ◗ ◘ ◙ ◚ ◛ ◜ ◝ ◞ ◟ ◠ ◡ ◢ ◣ ◤ ◥ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮ ◯
|
||||||
|
Miscellaneous Symbols
|
||||||
|
☀ ☁ ☂ ☃ ☄ ★ ☆ ☇ ☈ ☉ ☊ ☋ ☌ ☍ ☎ ☏ ☐ ☑ ☒ ☓ ☚ ☛ ☜ ☝ ☞ ☟ ☠ ☡ ☢ ☣ ☤ ☥ ☦ ☧ ☨ ☩ ☪ ☫ ☬ ☭ ☮ ☯ ☰ ☱ ☲ ☳ ☴ ☵ ☶ ☷ ☸ ☹ ☺ ☻ ☼ ☽ ☾ ☿ ♀ ♁ ♂ ♃ ♄ ♅ ♆ ♇ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ♔ ♕ ♖ ♗ ♘ ♙ ♚ ♛ ♜ ♝ ♞ ♟ ♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯
|
||||||
|
Dingbats
|
||||||
|
✁ ✂ ✃ ✄ ✆ ✇ ✈ ✉ ✌ ✍ ✎ ✏ ✐ ✑ ✒ ✓ ✔ ✕ ✖ ✗ ✘ ✙ ✚ ✛ ✜ ✝ ✞ ✟ ✠ ✡ ✢ ✣ ✤ ✥ ✦ ✧ ✩ ✪ ✫ ✬ ✭ ✮ ✯ ✰ ✱ ✲ ✳ ✴ ✵ ✶ ✷ ✸ ✹ ✺ ✻ ✼ ✽ ✾ ✿ ❀ ❁ ❂ ❃ ❄ ❅ ❆ ❇ ❈ ❉ ❊ ❋ ❍ ❏ ❐ ❑ ❒ ❖ ❘ ❙ ❚ ❛ ❜ ❝ ❞ ❡ ❢ ❣ ❤ ❥ ❦ ❧ ❶ ❷ ❸ ❹ ❺ ❻ ❼ ❽ ❾ ❿ ➀ ➁ ➂ ➃ ➄ ➅ ➆ ➇ ➈ ➉ ➊ ➋ ➌ ➍ ➎ ➏ ➐ ➑ ➒ ➓ ➔ ➘ ➙ ➚ ➛ ➜ ➝ ...
|
||||||
|
CJK Symbols and Punctuation
|
||||||
|
、 。 〃 〄 々 〆 〇 〈 〉 《 》 「 」 『 』 【 】 〒 〓 〔 〕 〖 〗 〘 〙 〚 〛 〜 〝 〞 〟 〠 〡 〢 〣 〤 〥 〦 〧 〨 〩 〪 〫 〬 〭 〮 〯 〰 〱 〲 〳 〴 〵 〶 〷 〿
|
||||||
|
Hiragana
|
||||||
|
ぁ あ ぃ い ぅ う ぇ え ぉ お か が き ぎ く ぐ け げ こ ご さ ざ し じ す ず せ ぜ そ ぞ た だ ち ぢ っ つ づ て で と ど な に ぬ ね の は ば ぱ ひ び ぴ ふ ぶ ぷ へ べ ぺ ほ ぼ ぽ ま み む め も ゃ や ゅ ゆ ょ よ ら り る れ ろ ゎ わ ゐ ゑ を ん ゔ ゙ ゚ ゛ ゜ ゝ ゞ
|
||||||
|
Katakana
|
||||||
|
ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ ゲ コ ゴ サ ザ シ ジ ス ズ セ ゼ ソ ゾ タ ダ チ ヂ ッ ツ ヅ テ デ ト ド ナ ニ ヌ ネ ノ ハ バ パ ヒ ビ ピ フ ブ プ ヘ ベ ペ ホ ボ ポ マ ミ ム メ モ ャ ヤ ュ ユ ョ ヨ ラ リ ル レ ロ ヮ ワ ヰ ヱ ヲ ン ヴ ヵ ヶ ヷ ヸ ヹ ヺ ・ ー ヽ ヾ
|
||||||
|
Bopomofo
|
||||||
|
ㄅ ㄆ ㄇ ㄈ ㄉ ㄊ ㄋ ㄌ ㄍ ㄎ ㄏ ㄐ ㄑ ㄒ ㄓ ㄔ ㄕ ㄖ ㄗ ㄘ ㄙ ㄚ ㄛ ㄜ ㄝ ㄞ ㄟ ㄠ ㄡ ㄢ ㄣ ㄤ ㄥ ㄦ ㄧ ㄨ ㄩ ㄪ ㄫ ㄬ
|
||||||
|
Hangul Compatibility Jamo
|
||||||
|
ㄱ ㄲ ㄳ ㄴ ㄵ ㄶ ㄷ ㄸ ㄹ ㄺ ㄻ ㄼ ㄽ ㄾ ㄿ ㅀ ㅁ ㅂ ㅃ ㅄ ㅅ ㅆ ㅇ ㅈ ㅉ ㅊ ㅋ ㅌ ㅍ ㅎ ㅏ ㅐ ㅑ ㅒ ㅓ ㅔ ㅕ ㅖ ㅗ ㅘ ㅙ ㅚ ㅛ ㅜ ㅝ ㅞ ㅟ ㅠ ㅡ ㅢ ㅣ ㅤ ㅥ ㅦ ㅧ ㅨ ㅩ ㅪ ㅫ ㅬ ㅭ ㅮ ㅯ ㅰ ㅱ ㅲ ㅳ ㅴ ㅵ ㅶ ㅷ ㅸ ㅹ ㅺ ㅻ ㅼ ㅽ ㅾ ㅿ ㆀ ㆁ ㆂ ㆃ ㆄ ㆅ ㆆ ㆇ ㆈ ㆉ ㆊ ㆋ ㆌ ㆍ ㆎ
|
||||||
|
Kanbun
|
||||||
|
㆐ ㆑ ㆒ ㆓ ㆔ ㆕ ㆖ ㆗ ㆘ ㆙ ㆚ ㆛ ㆜ ㆝ ㆞ ㆟
|
||||||
|
Enclosed CJK Letters and Months
|
||||||
|
㈀ ㈁ ㈂ ㈃ ㈄ ㈅ ㈆ ㈇ ㈈ ㈉ ㈊ ㈋ ㈌ ㈍ ㈎ ㈏ ㈐ ㈑ ㈒ ㈓ ㈔ ㈕ ㈖ ㈗ ㈘ ㈙ ㈚ ㈛ ㈜ ㈠ ㈡ ㈢ ㈣ ㈤ ㈥ ㈦ ㈧ ㈨ ㈩ ㈪ ㈫ ㈬ ㈭ ㈮ ㈯ ㈰ ㈱ ㈲ ㈳ ㈴ ㈵ ㈶ ㈷ ㈸ ㈹ ㈺ ㈻ ㈼ ㈽ ㈾ ㈿ ㉀ ㉁ ㉂ ㉃ ㉠ ㉡ ㉢ ㉣ ㉤ ㉥ ㉦ ㉧ ㉨ ㉩ ㉪ ㉫ ㉬ ㉭ ㉮ ㉯ ㉰ ㉱ ㉲ ㉳ ㉴ ㉵ ㉶ ㉷ ㉸ ㉹ ㉺ ㉻ ㉿ ㊀ ㊁ ㊂ ㊃ ㊄ ㊅ ㊆ ㊇ ㊈ ㊉ ㊊ ㊋ ㊌ ㊍ ㊎ ㊏ ㊐ ㊑ ㊒ ㊓ ㊔ ㊕ ㊖ ㊗ ㊘ ㊙ ㊚ ㊛ ㊜ ㊝ ㊞ ㊟ ㊠ ㊡ ...
|
||||||
|
CJK Compatibility
|
||||||
|
㌀ ㌁ ㌂ ㌃ ㌄ ㌅ ㌆ ㌇ ㌈ ㌉ ㌊ ㌋ ㌌ ㌍ ㌎ ㌏ ㌐ ㌑ ㌒ ㌓ ㌔ ㌕ ㌖ ㌗ ㌘ ㌙ ㌚ ㌛ ㌜ ㌝ ㌞ ㌟ ㌠ ㌡ ㌢ ㌣ ㌤ ㌥ ㌦ ㌧ ㌨ ㌩ ㌪ ㌫ ㌬ ㌭ ㌮ ㌯ ㌰ ㌱ ㌲ ㌳ ㌴ ㌵ ㌶ ㌷ ㌸ ㌹ ㌺ ㌻ ㌼ ㌽ ㌾ ㌿ ㍀ ㍁ ㍂ ㍃ ㍄ ㍅ ㍆ ㍇ ㍈ ㍉ ㍊ ㍋ ㍌ ㍍ ㍎ ㍏ ㍐ ㍑ ㍒ ㍓ ㍔ ㍕ ㍖ ㍗ ㍘ ㍙ ㍚ ㍛ ㍜ ㍝ ㍞ ㍟ ㍠ ㍡ ㍢ ㍣ ㍤ ㍥ ㍦ ㍧ ㍨ ㍩ ㍪ ㍫ ㍬ ㍭ ㍮ ㍯ ㍰ ㍱ ㍲ ㍳ ㍴ ㍵ ㍶ ㍻ ㍼ ㍽ ㍾ ㍿ ㎀ ㎁ ㎂ ㎃ ...
|
||||||
|
CJK Unified Ideographs
|
||||||
|
一 丁 丂 七 丄 丅 丆 万 丈 三 上 下 丌 不 与 丏 丐 丑 丒 专 且 丕 世 丗 丘 丙 业 丛 东 丝 丞 丟 丠 両 丢 丣 两 严 並 丧 丨 丩 个 丫 丬 中 丮 丯 丰 丱 串 丳 临 丵 丶 丷 丸 丹 为 主 丼 丽 举 丿 乀 乁 乂 乃 乄 久 乆 乇 么 义 乊 之 乌 乍 乎 乏 乐 乑 乒 乓 乔 乕 乖 乗 乘 乙 乚 乛 乜 九 乞 也 习 乡 乢 乣 乤 乥 书 乧 乨 乩 乪 乫 乬 乭 乮 乯 买 乱 乲 乳 乴 乵 乶 乷 乸 乹 乺 乻 乼 乽 乾 乿 ...
|
||||||
|
Hangul Syllables
|
||||||
|
가 각 갂 갃 간 갅 갆 갇 갈 갉 갊 갋 갌 갍 갎 갏 감 갑 값 갓 갔 강 갖 갗 갘 같 갚 갛 개 객 갞 갟 갠 갡 갢 갣 갤 갥 갦 갧 갨 갩 갪 갫 갬 갭 갮 갯 갰 갱 갲 갳 갴 갵 갶 갷 갸 갹 갺 갻 갼 갽 갾 갿 걀 걁 걂 걃 걄 걅 걆 걇 걈 걉 걊 걋 걌 걍 걎 걏 걐 걑 걒 걓 걔 걕 걖 걗 걘 걙 걚 걛 걜 걝 걞 걟 걠 걡 걢 걣 걤 걥 걦 걧 걨 걩 걪 걫 걬 걭 걮 걯 거 걱 걲 걳 건 걵 걶 걷 걸 걹 걺 걻 걼 걽 걾 걿 ...
|
||||||
|
Private Use
|
||||||
|
...
|
||||||
|
CJK Compatibility Ideographs
|
||||||
|
豈 更 車 賈 滑 串 句 龜 龜 契 金 喇 奈 懶 癩 羅 蘿 螺 裸 邏 樂 洛 烙 珞 落 酪 駱 亂 卵 欄 爛 蘭 鸞 嵐 濫 藍 襤 拉 臘 蠟 廊 朗 浪 狼 郎 來 冷 勞 擄 櫓 爐 盧 老 蘆 虜 路 露 魯 鷺 碌 祿 綠 菉 錄 鹿 論 壟 弄 籠 聾 牢 磊 賂 雷 壘 屢 樓 淚 漏 累 縷 陋 勒 肋 凜 凌 稜 綾 菱 陵 讀 拏 樂 諾 丹 寧 怒 率 異 北 磻 便 復 不 泌 數 索 參 塞 省 葉 說 殺 辰 沈 拾 若 掠 略 亮 兩 凉 梁 糧 良 諒 量 勵 ...
|
||||||
|
Alphabetic Presentation Forms
|
||||||
|
ff fi fl ffi ffl ſt st ﬓ ﬔ ﬕ ﬖ ﬗ ﬞ ײַ ﬠ ﬡ ﬢ ﬣ ﬤ ﬥ ﬦ ﬧ ﬨ ﬩ שׁ שׂ שּׁ שּׂ אַ אָ אּ בּ גּ דּ הּ וּ זּ טּ יּ ךּ כּ לּ מּ נּ סּ ףּ פּ צּ קּ רּ שּ תּ וֹ בֿ כֿ פֿ ﭏ
|
||||||
|
Arabic Presentation Forms-A
|
||||||
|
ﭐ ﭑ ﭒ ﭓ ﭔ ﭕ ﭖ ﭗ ﭘ ﭙ ﭚ ﭛ ﭜ ﭝ ﭞ ﭟ ﭠ ﭡ ﭢ ﭣ ﭤ ﭥ ﭦ ﭧ ﭨ ﭩ ﭪ ﭫ ﭬ ﭭ ﭮ ﭯ ﭰ ﭱ ﭲ ﭳ ﭴ ﭵ ﭶ ﭷ ﭸ ﭹ ﭺ ﭻ ﭼ ﭽ ﭾ ﭿ ﮀ ﮁ ﮂ ﮃ ﮄ ﮅ ﮆ ﮇ ﮈ ﮉ ﮊ ﮋ ﮌ ﮍ ﮎ ﮏ ﮐ ﮑ ﮒ ﮓ ﮔ ﮕ ﮖ ﮗ ﮘ ﮙ ﮚ ﮛ ﮜ ﮝ ﮞ ﮟ ﮠ ﮡ ﮢ ﮣ ﮤ ﮥ ﮦ ﮧ ﮨ ﮩ ﮪ ﮫ ﮬ ﮭ ﮮ ﮯ ﮰ ﮱ ﯓ ﯔ ﯕ ﯖ ﯗ ﯘ ﯙ ﯚ ﯛ ﯜ ﯝ ﯞ ﯟ ﯠ ﯡ ﯢ ﯣ ﯤ ﯥ ﯦ ﯧ ﯨ ﯩ ﯪ ﯫ ﯬ ﯭ ﯮ ﯯ ﯰ ...
|
||||||
|
Combining Half Marks
|
||||||
|
︠ ︡ ︢ ︣
|
||||||
|
CJK Compatibility Forms
|
||||||
|
︰ ︱ ︲ ︳ ︴ ︵ ︶ ︷ ︸ ︹ ︺ ︻ ︼ ︽ ︾ ︿ ﹀ ﹁ ﹂ ﹃ ﹄ ﹉ ﹊ ﹋ ﹌ ﹍ ﹎ ﹏
|
||||||
|
Small Form Variants
|
||||||
|
﹐ ﹑ ﹒ ﹔ ﹕ ﹖ ﹗ ﹘ ﹙ ﹚ ﹛ ﹜ ﹝ ﹞ ﹟ ﹠ ﹡ ﹢ ﹣ ﹤ ﹥ ﹦ ﹨ ﹩ ﹪ ﹫
|
||||||
|
Arabic Presentation Forms-B
|
||||||
|
ﹰ ﹱ ﹲ ﹴ ﹶ ﹷ ﹸ ﹹ ﹺ ﹻ ﹼ ﹽ ﹾ ﹿ ﺀ ﺁ ﺂ ﺃ ﺄ ﺅ ﺆ ﺇ ﺈ ﺉ ﺊ ﺋ ﺌ ﺍ ﺎ ﺏ ﺐ ﺑ ﺒ ﺓ ﺔ ﺕ ﺖ ﺗ ﺘ ﺙ ﺚ ﺛ ﺜ ﺝ ﺞ ﺟ ﺠ ﺡ ﺢ ﺣ ﺤ ﺥ ﺦ ﺧ ﺨ ﺩ ﺪ ﺫ ﺬ ﺭ ﺮ ﺯ ﺰ ﺱ ﺲ ﺳ ﺴ ﺵ ﺶ ﺷ ﺸ ﺹ ﺺ ﺻ ﺼ ﺽ ﺾ ﺿ ﻀ ﻁ ﻂ ﻃ ﻄ ﻅ ﻆ ﻇ ﻈ ﻉ ﻊ ﻋ ﻌ ﻍ ﻎ ﻏ ﻐ ﻑ ﻒ ﻓ ﻔ ﻕ ﻖ ﻗ ﻘ ﻙ ﻚ ﻛ ﻜ ﻝ ﻞ ﻟ ﻠ ﻡ ﻢ ﻣ ﻤ ﻥ ﻦ ﻧ ﻨ ﻩ ﻪ ﻫ ﻬ ﻭ ﻮ ﻯ ﻰ ﻱ ...
|
||||||
|
Halfwidth and Fullwidth Forms
|
||||||
|
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ 。 「 」 、 ・ ヲ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ ...
|
||||||
|
Specials
|
||||||
|
|
||||||
|
Specials
|
||||||
|
<20>
|
||||||
|
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package com.kunzisoft.keepass.tests.stream
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryByte
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryFile
|
||||||
|
import com.kunzisoft.keepass.stream.readAllBytes
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.DataInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class BinaryDataTest {
|
||||||
|
|
||||||
|
private val context: Context by lazy {
|
||||||
|
InstrumentationRegistry.getInstrumentation().context
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cacheDirectory = UriUtil.getBinaryDir(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||||
|
private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A)
|
||||||
|
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
|
||||||
|
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
|
||||||
|
|
||||||
|
private val loadedKey = Database.LoadedKey.generateNewCipherKey()
|
||||||
|
|
||||||
|
private fun saveBinary(asset: String, binaryData: BinaryFile) {
|
||||||
|
context.assets.open(asset).use { assetInputStream ->
|
||||||
|
binaryData.getOutputDataStream(loadedKey).use { binaryOutputStream ->
|
||||||
|
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
|
||||||
|
binaryOutputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSaveTextInCache() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
val binaryB = BinaryFile(fileB)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryB)
|
||||||
|
assertEquals("Save text binary length failed.", binaryA.getSize(), binaryB.getSize())
|
||||||
|
assertEquals("Save text binary MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSaveImageInCache() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
val binaryB = BinaryFile(fileB)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryB)
|
||||||
|
assertEquals("Save image binary length failed.", binaryA.getSize(), binaryB.getSize())
|
||||||
|
assertEquals("Save image binary failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCompressText() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
val binaryB = BinaryFile(fileB)
|
||||||
|
val binaryC = BinaryFile(fileC)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryB)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryC)
|
||||||
|
binaryA.compress(loadedKey)
|
||||||
|
binaryB.compress(loadedKey)
|
||||||
|
assertEquals("Compress text length failed.", binaryA.getSize(), binaryB.getSize())
|
||||||
|
assertEquals("Compress text MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
||||||
|
binaryB.decompress(loadedKey)
|
||||||
|
assertEquals("Decompress text length failed.", binaryB.getSize(), binaryC.getSize())
|
||||||
|
assertEquals("Decompress text MD5 failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCompressImage() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
var binaryB = BinaryFile(fileB)
|
||||||
|
val binaryC = BinaryFile(fileC)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryB)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryC)
|
||||||
|
binaryA.compress(loadedKey)
|
||||||
|
binaryB.compress(loadedKey)
|
||||||
|
assertEquals("Compress image length failed.", binaryA.getSize(), binaryA.getSize())
|
||||||
|
assertEquals("Compress image failed.", binaryA.binaryHash(), binaryA.binaryHash())
|
||||||
|
binaryB = BinaryFile(fileB, true)
|
||||||
|
binaryB.decompress(loadedKey)
|
||||||
|
assertEquals("Decompress image length failed.", binaryB.getSize(), binaryC.getSize())
|
||||||
|
assertEquals("Decompress image failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCompressBytes() {
|
||||||
|
val byteArray = ByteArray(50)
|
||||||
|
Random.nextBytes(byteArray)
|
||||||
|
val binaryA = BinaryByte(byteArray)
|
||||||
|
val binaryB = BinaryByte(byteArray)
|
||||||
|
val binaryC = BinaryByte(byteArray)
|
||||||
|
binaryA.compress(loadedKey)
|
||||||
|
binaryB.compress(loadedKey)
|
||||||
|
assertEquals("Compress bytes decompressed failed.", binaryA.isCompressed, true)
|
||||||
|
assertEquals("Compress bytes length failed.", binaryA.getSize(), binaryA.getSize())
|
||||||
|
assertEquals("Compress bytes failed.", binaryA.binaryHash(), binaryA.binaryHash())
|
||||||
|
binaryB.decompress(loadedKey)
|
||||||
|
assertEquals("Decompress bytes decompressed failed.", binaryB.isCompressed, false)
|
||||||
|
assertEquals("Decompress bytes length failed.", binaryB.getSize(), binaryC.getSize())
|
||||||
|
assertEquals("Decompress bytes failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReadText() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||||
|
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
|
||||||
|
binaryA.getInputDataStream(loadedKey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReadImage() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||||
|
assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET),
|
||||||
|
binaryA.getInputDataStream(loadedKey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun streamAreEquals(inputStreamA: InputStream,
|
||||||
|
inputStreamB: InputStream): Boolean {
|
||||||
|
val bufferA = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
val bufferB = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
val dataInputStreamB = DataInputStream(inputStreamB)
|
||||||
|
try {
|
||||||
|
var len: Int
|
||||||
|
while (inputStreamA.read(bufferA).also { len = it } > 0) {
|
||||||
|
dataInputStreamB.readFully(bufferB, 0, len)
|
||||||
|
for (i in 0 until len) {
|
||||||
|
if (bufferA[i] != bufferB[i])
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inputStreamB.read() < 0 // is the end of the second file also.
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inputStreamA.close()
|
||||||
|
inputStreamB.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TEST_FILE_CACHE_A = "testA"
|
||||||
|
private const val TEST_FILE_CACHE_B = "testB"
|
||||||
|
private const val TEST_FILE_CACHE_C = "testC"
|
||||||
|
private const val TEST_IMAGE_ASSET = "test_image.png"
|
||||||
|
private const val TEST_TEXT_ASSET = "test_text.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,9 +129,15 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||||
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<!-- About and Settings -->
|
<!-- About and Settings -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||||
@@ -175,19 +181,19 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.AttachmentFileNotificationService"
|
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService"
|
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<!-- Receiver for Autofill -->
|
<!-- Receiver for Autofill -->
|
||||||
@@ -213,7 +219,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
|||||||
1061
app/src/main/java/com/igreenwood/loupe/Loupe.kt
Normal file
1061
app/src/main/java/com/igreenwood/loupe/Loupe.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -47,26 +47,22 @@ import com.kunzisoft.keepass.database.element.Database
|
|||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
import com.kunzisoft.keepass.utils.createDocument
|
|
||||||
import com.kunzisoft.keepass.utils.onCreateDocumentResult
|
|
||||||
import com.kunzisoft.keepass.view.EntryContentsView
|
import com.kunzisoft.keepass.view.EntryContentsView
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
@@ -129,6 +125,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
historyView = findViewById(R.id.history_container)
|
historyView = findViewById(R.id.history_container)
|
||||||
entryContentsView = findViewById(R.id.entry_contents)
|
entryContentsView = findViewById(R.id.entry_contents)
|
||||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||||
|
entryContentsView?.setAttachmentCipherKey(mDatabase?.loadedCipherKey)
|
||||||
entryProgress = findViewById(R.id.entry_progress)
|
entryProgress = findViewById(R.id.entry_progress)
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
|
||||||
@@ -156,10 +153,11 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
ACTION_DATABASE_RELOAD_TASK -> {
|
ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
// Close the current activity
|
// Close the current activity
|
||||||
|
this.showActionErrorIfNeeded(result)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionError(result)
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +220,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
registerProgressTask()
|
registerProgressTask()
|
||||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
entryContentsView?.putAttachment(entryAttachmentState)
|
if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
|
||||||
|
entryContentsView?.putAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +241,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||||
|
|
||||||
// Assign title icon
|
// Assign title icon
|
||||||
titleIconView?.assignDatabaseIcon(mDatabase!!.drawFactory, entryInfo.icon, iconColor)
|
titleIconView?.let { iconView ->
|
||||||
|
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
|
||||||
|
}
|
||||||
|
|
||||||
// Assign title text
|
// Assign title text
|
||||||
val entryTitle = entryInfo.title
|
val entryTitle = entryInfo.title
|
||||||
@@ -349,7 +351,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
// Assign dates
|
// Assign dates
|
||||||
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
||||||
entryContentsView?.assignModificationDate(entryInfo.modificationTime)
|
entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
|
||||||
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
||||||
|
|
||||||
// Manage history
|
// Manage history
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import android.widget.DatePicker
|
|||||||
import android.widget.TimePicker
|
import android.widget.TimePicker
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
@@ -44,6 +43,7 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
@@ -57,29 +57,28 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.model.*
|
import com.kunzisoft.keepass.model.*
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
|
||||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.view.ToolbarAction
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class EntryEditActivity : LockingActivity(),
|
class EntryEditActivity : LockingActivity(),
|
||||||
IconPickerDialogFragment.IconPickerListener,
|
|
||||||
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||||
SetOTPDialogFragment.CreateOtpListener,
|
SetOTPDialogFragment.CreateOtpListener,
|
||||||
@@ -99,7 +98,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var scrollView: NestedScrollView? = null
|
private var scrollView: NestedScrollView? = null
|
||||||
private var entryEditFragment: EntryEditFragment? = null
|
private var entryEditFragment: EntryEditFragment? = null
|
||||||
private var entryEditAddToolBar: Toolbar? = null
|
private var entryEditAddToolBar: ToolbarAction? = null
|
||||||
private var validateButton: View? = null
|
private var validateButton: View? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
|
|
||||||
@@ -107,7 +106,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
private var mAllowMultipleAttachments: Boolean = false
|
private var mAllowMultipleAttachments: Boolean = false
|
||||||
private var mTempAttachments = ArrayList<Attachment>()
|
private var mTempAttachments = ArrayList<EntryAttachmentState>()
|
||||||
|
|
||||||
// Education
|
// Education
|
||||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||||
@@ -119,11 +118,12 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_entry_edit)
|
setContentView(R.layout.activity_entry_edit)
|
||||||
|
|
||||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
// Bottom Bar
|
||||||
setSupportActionBar(toolbar)
|
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||||
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp)
|
setSupportActionBar(entryEditAddToolBar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
|
|
||||||
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
|
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
|
||||||
|
|
||||||
@@ -172,10 +172,14 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
val parentIcon = mParent?.icon
|
val parentIcon = mParent?.icon
|
||||||
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
|
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
|
||||||
// Set default icon
|
// Set default icon
|
||||||
if (parentIcon != null
|
if (parentIcon != null) {
|
||||||
&& parentIcon.iconId != IconImage.UNKNOWN_ID
|
if (parentIcon.custom.isUnknown
|
||||||
&& parentIcon.iconId != IconImageStandard.FOLDER) {
|
&& parentIcon.standard.id != IconImageStandard.FOLDER_ID) {
|
||||||
tempEntryInfo?.icon = parentIcon
|
tempEntryInfo?.icon = IconImage(parentIcon.standard)
|
||||||
|
}
|
||||||
|
if (!parentIcon.custom.isUnknown) {
|
||||||
|
tempEntryInfo?.icon = IconImage(parentIcon.custom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Set default username
|
// Set default username
|
||||||
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
|
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
|
||||||
@@ -198,14 +202,14 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
// Build fragment to manage entry modification
|
// Build fragment to manage entry modification
|
||||||
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
|
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
|
||||||
if (entryEditFragment == null) {
|
if (entryEditFragment == null) {
|
||||||
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
|
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo, mDatabase?.loadedCipherKey)
|
||||||
}
|
}
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
||||||
.commit()
|
.commit()
|
||||||
entryEditFragment?.apply {
|
entryEditFragment?.apply {
|
||||||
drawFactory = mDatabase?.drawFactory
|
drawFactory = mDatabase?.iconDrawableFactory
|
||||||
setOnDateClickListener = View.OnClickListener {
|
setOnDateClickListener = {
|
||||||
expiryTime.date.let { expiresDate ->
|
expiryTime.date.let { expiresDate ->
|
||||||
val dateTime = DateTime(expiresDate)
|
val dateTime = DateTime(expiresDate)
|
||||||
val defaultYear = dateTime.year
|
val defaultYear = dateTime.year
|
||||||
@@ -219,8 +223,8 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
openPasswordGenerator()
|
openPasswordGenerator()
|
||||||
}
|
}
|
||||||
// Add listener to the icon
|
// Add listener to the icon
|
||||||
setOnIconViewClickListener = View.OnClickListener {
|
setOnIconViewClickListener = { iconImage ->
|
||||||
IconPickerDialogFragment.launch(this@EntryEditActivity)
|
IconPickerActivity.launch(this@EntryEditActivity, iconImage)
|
||||||
}
|
}
|
||||||
setOnRemoveAttachment = { attachment ->
|
setOnRemoveAttachment = { attachment ->
|
||||||
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
|
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
|
||||||
@@ -236,51 +240,6 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
|
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign title
|
|
||||||
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
|
|
||||||
|
|
||||||
// Bottom Bar
|
|
||||||
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
|
||||||
entryEditAddToolBar?.apply {
|
|
||||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
|
||||||
|
|
||||||
menu.findItem(R.id.menu_add_field).apply {
|
|
||||||
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
|
|
||||||
isEnabled = allowCustomField
|
|
||||||
isVisible = allowCustomField
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attachment not compatible below KitKat
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
|
||||||
menu.findItem(R.id.menu_add_attachment).isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.findItem(R.id.menu_add_otp).apply {
|
|
||||||
val allowOTP = mDatabase?.allowOTP == true
|
|
||||||
isEnabled = allowOTP
|
|
||||||
// OTP not compatible below KitKat
|
|
||||||
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
|
||||||
}
|
|
||||||
|
|
||||||
setOnMenuItemClickListener { item ->
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.menu_add_field -> {
|
|
||||||
addNewCustomField()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_add_attachment -> {
|
|
||||||
addNewAttachment(item)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_add_otp -> {
|
|
||||||
setupOTP()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// To retrieve attachment
|
// To retrieve attachment
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
@@ -338,10 +297,11 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
ACTION_DATABASE_RELOAD_TASK -> {
|
ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
// Close the current activity
|
// Close the current activity
|
||||||
|
this.showActionErrorIfNeeded(result)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionError(result)
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,29 +358,27 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
when (entryAttachmentState.downloadState) {
|
when (entryAttachmentState.downloadState) {
|
||||||
AttachmentState.START -> {
|
AttachmentState.START -> {
|
||||||
entryEditFragment?.apply {
|
entryEditFragment?.apply {
|
||||||
// When only one attachment is allowed
|
|
||||||
if (!mAllowMultipleAttachments) {
|
|
||||||
clearAttachments()
|
|
||||||
}
|
|
||||||
putAttachment(entryAttachmentState)
|
putAttachment(entryAttachmentState)
|
||||||
// Scroll to the attachment position
|
// Scroll to the attachment position
|
||||||
getAttachmentViewPosition(entryAttachmentState) {
|
getAttachmentViewPosition(entryAttachmentState) {
|
||||||
scrollView?.smoothScrollTo(0, it.toInt())
|
scrollView?.smoothScrollTo(0, it.toInt())
|
||||||
}
|
}
|
||||||
}
|
} // Add in temp list
|
||||||
|
mTempAttachments.add(entryAttachmentState)
|
||||||
}
|
}
|
||||||
AttachmentState.IN_PROGRESS -> {
|
AttachmentState.IN_PROGRESS -> {
|
||||||
entryEditFragment?.putAttachment(entryAttachmentState)
|
entryEditFragment?.putAttachment(entryAttachmentState)
|
||||||
}
|
}
|
||||||
AttachmentState.COMPLETE -> {
|
AttachmentState.COMPLETE -> {
|
||||||
entryEditFragment?.apply {
|
entryEditFragment?.putAttachment(entryAttachmentState) {
|
||||||
putAttachment(entryAttachmentState)
|
entryEditFragment?.getAttachmentViewPosition(entryAttachmentState) {
|
||||||
// Scroll to the attachment position
|
|
||||||
getAttachmentViewPosition(entryAttachmentState) {
|
|
||||||
scrollView?.smoothScrollTo(0, it.toInt())
|
scrollView?.smoothScrollTo(0, it.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AttachmentState.CANCELED -> {
|
||||||
|
entryEditFragment?.removeAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
AttachmentState.ERROR -> {
|
AttachmentState.ERROR -> {
|
||||||
entryEditFragment?.removeAttachment(entryAttachmentState)
|
entryEditFragment?.removeAttachment(entryAttachmentState)
|
||||||
coordinatorLayout?.let {
|
coordinatorLayout?.let {
|
||||||
@@ -516,16 +474,18 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||||
if (attachmentToUploadUri != null && attachment != null) {
|
if (attachmentToUploadUri != null && attachment != null) {
|
||||||
|
// When only one attachment is allowed
|
||||||
|
if (!mAllowMultipleAttachments) {
|
||||||
|
entryEditFragment?.clearAttachments()
|
||||||
|
}
|
||||||
// Start uploading in service
|
// Start uploading in service
|
||||||
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
||||||
// Add in temp list
|
|
||||||
mTempAttachments.add(attachment)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
||||||
val compression = mDatabase?.compressionForNewEntry() ?: false
|
val compression = mDatabase?.compressionForNewEntry() ?: false
|
||||||
mDatabase?.buildNewBinary(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment ->
|
mDatabase?.buildNewBinaryAttachment(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment ->
|
||||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
// Ask to replace the current attachment
|
// Ask to replace the current attachment
|
||||||
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
||||||
@@ -541,9 +501,12 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
|
||||||
|
entryEditFragment?.icon = icon
|
||||||
|
}
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
uri?.let { attachmentToUploadUri ->
|
uri?.let { attachmentToUploadUri ->
|
||||||
// TODO Async to get the name
|
|
||||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||||
documentFile.name?.let { fileName ->
|
documentFile.name?.let { fileName ->
|
||||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||||
@@ -572,6 +535,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
* Saves the new entry or update an existing entry in the database
|
* Saves the new entry or update an existing entry in the database
|
||||||
*/
|
*/
|
||||||
private fun saveEntry() {
|
private fun saveEntry() {
|
||||||
|
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||||
// Get the temp entry
|
// Get the temp entry
|
||||||
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
|
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
|
||||||
|
|
||||||
@@ -583,14 +547,34 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
Entry(mEntry!!)
|
Entry(mEntry!!)
|
||||||
}?.let { newEntry ->
|
}?.let { newEntry ->
|
||||||
|
|
||||||
|
// Do not save entry in upload progression
|
||||||
|
mTempAttachments.forEach { attachmentState ->
|
||||||
|
if (attachmentState.streamDirection == StreamDirection.UPLOAD) {
|
||||||
|
when (attachmentState.downloadState) {
|
||||||
|
AttachmentState.START,
|
||||||
|
AttachmentState.IN_PROGRESS,
|
||||||
|
AttachmentState.CANCELED,
|
||||||
|
AttachmentState.ERROR -> {
|
||||||
|
// Remove attachment not finished from info
|
||||||
|
newEntryInfo.attachments = newEntryInfo.attachments.toMutableList().apply {
|
||||||
|
remove(attachmentState.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build info
|
// Build info
|
||||||
newEntry.setEntryInfo(mDatabase, newEntryInfo)
|
newEntry.setEntryInfo(mDatabase, newEntryInfo)
|
||||||
|
|
||||||
// Delete temp attachment if not used
|
// Delete temp attachment if not used
|
||||||
mTempAttachments.forEach {
|
mTempAttachments.forEach { tempAttachmentState ->
|
||||||
mDatabase?.binaryPool?.let { binaryPool ->
|
val tempAttachment = tempAttachmentState.attachment
|
||||||
if (!newEntry.getAttachments(binaryPool).contains(it)) {
|
mDatabase?.attachmentPool?.let { binaryPool ->
|
||||||
mDatabase?.removeAttachmentIfNotUsed(it)
|
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
|
||||||
|
mDatabase?.removeAttachmentIfNotUsed(tempAttachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -619,12 +603,30 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
MenuUtil.contributionMenuInflater(menuInflater, menu)
|
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
|
||||||
|
menu?.findItem(R.id.menu_add_field)?.apply {
|
||||||
|
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
|
||||||
|
isEnabled = allowCustomField
|
||||||
|
isVisible = allowCustomField
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment not compatible below KitKat
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
menu?.findItem(R.id.menu_add_attachment)?.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
menu?.findItem(R.id.menu_add_otp)?.apply {
|
||||||
|
val allowOTP = mDatabase?.allowOTP == true
|
||||||
|
isEnabled = allowOTP
|
||||||
|
// OTP not compatible below KitKat
|
||||||
|
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||||
|
}
|
||||||
|
|
||||||
entryEditActivityEducation?.let {
|
entryEditActivityEducation?.let {
|
||||||
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||||
}
|
}
|
||||||
@@ -676,8 +678,16 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_contribute -> {
|
R.id.menu_add_field -> {
|
||||||
MenuUtil.onContributionItemSelected(this)
|
addNewCustomField()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.menu_add_attachment -> {
|
||||||
|
addNewAttachment(item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.menu_add_otp -> {
|
||||||
|
setupOTP()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
@@ -708,12 +718,6 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iconPicked(bundle: Bundle) {
|
|
||||||
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
|
|
||||||
entryEditFragment?.icon = icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
||||||
// To fix android 4.4 issue
|
// To fix android 4.4 issue
|
||||||
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||||
@@ -787,6 +791,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
.setMessage(R.string.discard_changes)
|
.setMessage(R.string.discard_changes)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.discard) { _, _ ->
|
.setPositiveButton(R.string.discard) { _, _ ->
|
||||||
|
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||||
backPressedAlreadyApproved = true
|
backPressedAlreadyApproved = true
|
||||||
approved.invoke()
|
approved.invoke()
|
||||||
}.create().show()
|
}.create().show()
|
||||||
|
|||||||
@@ -52,24 +52,24 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
|
|||||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
private var createDatabaseButtonView: View? = null
|
private var createDatabaseButtonView: View? = null
|
||||||
private var openDatabaseButtonView: View? = null
|
private var openDatabaseButtonView: View? = null
|
||||||
|
|
||||||
@@ -199,8 +199,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||||
val keyFileUri = result.data?.getParcelable<Uri?>(KEY_FILE_URI_KEY)
|
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
||||||
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
|
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
@@ -216,7 +216,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
resultError = "$resultError $resultMessage"
|
resultError = "$resultError $resultMessage"
|
||||||
}
|
}
|
||||||
Log.e(TAG, resultError)
|
Log.e(TAG, resultError)
|
||||||
Snackbar.make(activity_file_selection_coordinator_layout,
|
Snackbar.make(coordinatorLayout,
|
||||||
resultError,
|
resultError,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
@@ -237,9 +237,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||||
val error = getString(R.string.file_not_found_content)
|
val error = getString(R.string.file_not_found_content)
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
coordinatorLayout?.let {
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||||
@@ -330,9 +328,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(
|
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
@@ -340,24 +336,17 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
// Create the new database
|
// Create the new database
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPasswordChecked,
|
mainCredential
|
||||||
masterPassword,
|
|
||||||
keyFileChecked,
|
|
||||||
keyFile
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val error = getString(R.string.error_create_database_file)
|
val error = getString(R.string.error_create_database_file)
|
||||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogNegativeClick(
|
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
@@ -380,9 +369,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
.show(supportFragmentManager, "passwordDialog")
|
.show(supportFragmentManager, "passwordDialog")
|
||||||
} else {
|
} else {
|
||||||
val error = getString(R.string.error_create_database)
|
val error = getString(R.string.error_create_database)
|
||||||
coordinatorLayout?.let {
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
}
|
|
||||||
Log.e(TAG, error)
|
Log.e(TAG, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.DatePickerDialog
|
||||||
import android.app.SearchManager
|
import android.app.SearchManager
|
||||||
|
import android.app.TimePickerDialog
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -33,9 +35,7 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.*
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
@@ -45,6 +45,7 @@ import androidx.fragment.app.FragmentManager
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.ListNodesFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
@@ -53,37 +54,35 @@ import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrCha
|
|||||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.view.*
|
import com.kunzisoft.keepass.view.*
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class GroupActivity : LockingActivity(),
|
class GroupActivity : LockingActivity(),
|
||||||
GroupEditDialogFragment.EditGroupListener,
|
GroupEditDialogFragment.EditGroupListener,
|
||||||
IconPickerDialogFragment.IconPickerListener,
|
DatePickerDialog.OnDateSetListener,
|
||||||
|
TimePickerDialog.OnTimeSetListener,
|
||||||
ListNodesFragment.NodeClickListener,
|
ListNodesFragment.NodeClickListener,
|
||||||
ListNodesFragment.NodesActionMenuListener,
|
ListNodesFragment.NodesActionMenuListener,
|
||||||
DeleteNodesDialogFragment.DeleteNodeListener,
|
DeleteNodesDialogFragment.DeleteNodeListener,
|
||||||
@@ -105,7 +104,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private var mListNodesFragment: ListNodesFragment? = null
|
private var mListNodesFragment: ListNodesFragment? = null
|
||||||
private var mCurrentGroupIsASearch: Boolean = false
|
|
||||||
private var mRequestStartupSearch = true
|
private var mRequestStartupSearch = true
|
||||||
|
|
||||||
private var actionNodeMode: ActionMode? = null
|
private var actionNodeMode: ActionMode? = null
|
||||||
@@ -172,7 +170,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState)
|
mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState)
|
||||||
mCurrentGroupIsASearch = Intent.ACTION_SEARCH == intent.action
|
val currentGroupIsASearch = mCurrentGroup?.isVirtual == true
|
||||||
|
|
||||||
Log.i(TAG, "Started creating tree")
|
Log.i(TAG, "Started creating tree")
|
||||||
if (mCurrentGroup == null) {
|
if (mCurrentGroup == null) {
|
||||||
@@ -181,13 +179,13 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fragmentTag = LIST_NODES_FRAGMENT_TAG
|
var fragmentTag = LIST_NODES_FRAGMENT_TAG
|
||||||
if (mCurrentGroupIsASearch)
|
if (currentGroupIsASearch)
|
||||||
fragmentTag = SEARCH_FRAGMENT_TAG
|
fragmentTag = SEARCH_FRAGMENT_TAG
|
||||||
|
|
||||||
// Initialize the fragment with the list
|
// Initialize the fragment with the list
|
||||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
|
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
|
||||||
if (mListNodesFragment == null)
|
if (mListNodesFragment == null)
|
||||||
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, mCurrentGroupIsASearch)
|
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, currentGroupIsASearch)
|
||||||
|
|
||||||
// Attach fragment to content view
|
// Attach fragment to content view
|
||||||
supportFragmentManager.beginTransaction().replace(
|
supportFragmentManager.beginTransaction().replace(
|
||||||
@@ -206,9 +204,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
// Add listeners to the add buttons
|
// Add listeners to the add buttons
|
||||||
addNodeButtonView?.setAddGroupClickListener {
|
addNodeButtonView?.setAddGroupClickListener {
|
||||||
GroupEditDialogFragment.build()
|
GroupEditDialogFragment.create(GroupInfo().apply {
|
||||||
.show(supportFragmentManager,
|
if (mCurrentGroup?.allowAddNoteInGroup == true) {
|
||||||
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
notes = ""
|
||||||
|
}
|
||||||
|
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||||
}
|
}
|
||||||
addNodeButtonView?.setAddEntryClickListener {
|
addNodeButtonView?.setAddEntryClickListener {
|
||||||
mCurrentGroup?.let { currentGroup ->
|
mCurrentGroup?.let { currentGroup ->
|
||||||
@@ -345,13 +345,16 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
ACTION_DATABASE_RELOAD_TASK -> {
|
ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
// Reload the current activity
|
// Reload the current activity
|
||||||
startActivity(intent)
|
if (result.isSuccess) {
|
||||||
finish()
|
reload()
|
||||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
} else {
|
||||||
|
this.showActionErrorIfNeeded(result)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coordinatorLayout?.showActionError(result)
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
|
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
|
|
||||||
@@ -362,6 +365,14 @@ class GroupActivity : LockingActivity(),
|
|||||||
Log.i(TAG, "Finished creating tree")
|
Log.i(TAG, "Finished creating tree")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun reload() {
|
||||||
|
// Reload the current activity
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||||
|
mDatabase?.wasReloaded = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
@@ -370,13 +381,10 @@ class GroupActivity : LockingActivity(),
|
|||||||
manageSearchInfoIntent(intentNotNull)
|
manageSearchInfoIntent(intentNotNull)
|
||||||
Log.d(TAG, "setNewIntent: $intentNotNull")
|
Log.d(TAG, "setNewIntent: $intentNotNull")
|
||||||
setIntent(intentNotNull)
|
setIntent(intentNotNull)
|
||||||
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) {
|
if (Intent.ACTION_SEARCH == intentNotNull.action) {
|
||||||
// only one instance of search in backstack
|
// only one instance of search in backstack
|
||||||
deletePreviousSearchGroup()
|
deletePreviousSearchGroup()
|
||||||
openGroup(retrieveCurrentGroup(intentNotNull, null), true)
|
openGroup(retrieveCurrentGroup(intentNotNull, null), true)
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,12 +468,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
private fun refreshSearchGroup() {
|
private fun refreshSearchGroup() {
|
||||||
deletePreviousSearchGroup()
|
deletePreviousSearchGroup()
|
||||||
if (mCurrentGroupIsASearch)
|
if (mCurrentGroup?.isVirtual == true)
|
||||||
openGroup(retrieveCurrentGroup(intent, null), true)
|
openGroup(retrieveCurrentGroup(intent, null), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? {
|
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? {
|
||||||
|
|
||||||
// Force read only if the database is like that
|
// Force read only if the database is like that
|
||||||
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
|
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
|
||||||
|
|
||||||
@@ -513,24 +520,21 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mCurrentGroupIsASearch) {
|
|
||||||
searchTitleView?.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
searchTitleView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign icon
|
if (mCurrentGroup?.isVirtual == true) {
|
||||||
if (mCurrentGroupIsASearch) {
|
searchTitleView?.visibility = View.VISIBLE
|
||||||
if (toolbar != null) {
|
if (toolbar != null) {
|
||||||
toolbar?.navigationIcon = null
|
toolbar?.navigationIcon = null
|
||||||
}
|
}
|
||||||
iconView?.visibility = View.GONE
|
iconView?.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
searchTitleView?.visibility = View.GONE
|
||||||
// Assign the group icon depending of IconPack or custom icon
|
// Assign the group icon depending of IconPack or custom icon
|
||||||
iconView?.visibility = View.VISIBLE
|
iconView?.visibility = View.VISIBLE
|
||||||
mCurrentGroup?.let {
|
mCurrentGroup?.let { currentGroup ->
|
||||||
if (mDatabase?.drawFactory != null)
|
iconView?.let { imageView ->
|
||||||
iconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, it.icon, mIconColor)
|
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(imageView, currentGroup.icon, mIconColor)
|
||||||
|
}
|
||||||
|
|
||||||
if (toolbar != null) {
|
if (toolbar != null) {
|
||||||
if (mCurrentGroup?.containsParent() == true)
|
if (mCurrentGroup?.containsParent() == true)
|
||||||
@@ -545,20 +549,25 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Assign number of children
|
// Assign number of children
|
||||||
refreshNumberOfChildren()
|
refreshNumberOfChildren()
|
||||||
|
|
||||||
// Show button if allowed
|
// Hide button
|
||||||
addNodeButtonView?.apply {
|
initAddButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initAddButton() {
|
||||||
|
addNodeButtonView?.apply {
|
||||||
|
closeButtonIfOpen()
|
||||||
// To enable add button
|
// To enable add button
|
||||||
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
val addGroupEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true
|
||||||
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
var addEntryEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true
|
||||||
mCurrentGroup?.let {
|
mCurrentGroup?.let {
|
||||||
if (!it.allowAddEntryIfIsRoot())
|
if (!it.allowAddEntryIfIsRoot)
|
||||||
addEntryEnabled = it != mRootGroup && addEntryEnabled
|
addEntryEnabled = it != mRootGroup && addEntryEnabled
|
||||||
}
|
}
|
||||||
enableAddGroup(addGroupEnabled)
|
enableAddGroup(addGroupEnabled)
|
||||||
enableAddEntry(addEntryEnabled)
|
enableAddEntry(addEntryEnabled)
|
||||||
|
if (mCurrentGroup?.isVirtual == true)
|
||||||
if (actionNodeMode == null)
|
hideButton()
|
||||||
|
else if (actionNodeMode == null)
|
||||||
showButton()
|
showButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -700,6 +709,39 @@ class GroupActivity : LockingActivity(),
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
||||||
|
// To fix android 4.4 issue
|
||||||
|
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||||
|
if (datePicker?.isShown == true) {
|
||||||
|
val groupEditFragment = supportFragmentManager.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as? GroupEditDialogFragment
|
||||||
|
groupEditFragment?.getExpiryTime()?.date?.let { expiresDate ->
|
||||||
|
groupEditFragment.setExpiryTime(DateInstant(DateTime(expiresDate)
|
||||||
|
.withYear(year)
|
||||||
|
.withMonthOfYear(month + 1)
|
||||||
|
.withDayOfMonth(day)
|
||||||
|
.toDate()))
|
||||||
|
// Launch the time picker
|
||||||
|
val dateTime = DateTime(expiresDate)
|
||||||
|
val defaultHour = dateTime.hourOfDay
|
||||||
|
val defaultMinute = dateTime.minuteOfHour
|
||||||
|
TimePickerFragment.getInstance(defaultHour, defaultMinute)
|
||||||
|
.show(supportFragmentManager, "TimePickerFragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimeSet(view: TimePicker?, hours: Int, minutes: Int) {
|
||||||
|
val groupEditFragment = supportFragmentManager.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as? GroupEditDialogFragment
|
||||||
|
groupEditFragment?.getExpiryTime()?.date?.let { expiresDate ->
|
||||||
|
// Save the date
|
||||||
|
groupEditFragment.setExpiryTime(
|
||||||
|
DateInstant(DateTime(expiresDate)
|
||||||
|
.withHourOfDay(hours)
|
||||||
|
.withMinuteOfHour(minutes)
|
||||||
|
.toDate()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun finishNodeAction() {
|
private fun finishNodeAction() {
|
||||||
actionNodeMode?.finish()
|
actionNodeMode?.finish()
|
||||||
}
|
}
|
||||||
@@ -745,7 +787,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
when (node.type) {
|
when (node.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
mOldGroupToUpdate = node as Group
|
mOldGroupToUpdate = node as Group
|
||||||
GroupEditDialogFragment.build(mOldGroupToUpdate!!)
|
GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo())
|
||||||
.show(supportFragmentManager,
|
.show(supportFragmentManager,
|
||||||
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||||
}
|
}
|
||||||
@@ -855,6 +897,9 @@ class GroupActivity : LockingActivity(),
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
if (mDatabase?.wasReloaded == true) {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
// Show the lock button
|
// Show the lock button
|
||||||
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
@@ -1031,19 +1076,17 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
|
||||||
name: String?,
|
groupInfo: GroupInfo) {
|
||||||
icon: IconImage?) {
|
|
||||||
|
|
||||||
if (name != null && name.isNotEmpty() && icon != null) {
|
if (groupInfo.title.isNotEmpty()) {
|
||||||
when (action) {
|
when (action) {
|
||||||
GroupEditDialogFragment.EditGroupDialogAction.CREATION -> {
|
GroupEditDialogFragment.EditGroupDialogAction.CREATION -> {
|
||||||
// If group creation
|
// If group creation
|
||||||
mCurrentGroup?.let { currentGroup ->
|
mCurrentGroup?.let { currentGroup ->
|
||||||
// Build the group
|
// Build the group
|
||||||
mDatabase?.createGroup()?.let { newGroup ->
|
mDatabase?.createGroup()?.let { newGroup ->
|
||||||
newGroup.title = name
|
newGroup.setGroupInfo(groupInfo)
|
||||||
newGroup.icon = icon
|
|
||||||
// Not really needed here because added in runnable but safe
|
// Not really needed here because added in runnable but safe
|
||||||
newGroup.parent = currentGroup
|
newGroup.parent = currentGroup
|
||||||
|
|
||||||
@@ -1063,9 +1106,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
// WARNING remove parent and children to keep memory
|
// WARNING remove parent and children to keep memory
|
||||||
removeParent()
|
removeParent()
|
||||||
removeChildren()
|
removeChildren()
|
||||||
|
this.setGroupInfo(groupInfo)
|
||||||
title = name
|
|
||||||
this.icon = icon // TODO custom icon #96
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If group updated save it in the database
|
// If group updated save it in the database
|
||||||
@@ -1081,19 +1122,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
|
||||||
name: String?,
|
groupInfo: GroupInfo) {
|
||||||
icon: IconImage?) {
|
|
||||||
// Do nothing here
|
// Do nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
override// For icon in create tree dialog
|
|
||||||
fun iconPicked(bundle: Bundle) {
|
|
||||||
(supportFragmentManager
|
|
||||||
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment)
|
|
||||||
.iconPicked(bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||||
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
|
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
|
||||||
}
|
}
|
||||||
@@ -1132,6 +1165,13 @@ class GroupActivity : LockingActivity(),
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
// To create tree dialog for icon
|
||||||
|
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
|
||||||
|
(supportFragmentManager
|
||||||
|
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment)
|
||||||
|
.setIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
@@ -1156,7 +1196,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
mCurrentGroup = mListNodesFragment?.mainGroup
|
mCurrentGroup = mListNodesFragment?.mainGroup
|
||||||
// Remove search in intent
|
// Remove search in intent
|
||||||
deletePreviousSearchGroup()
|
deletePreviousSearchGroup()
|
||||||
mCurrentGroupIsASearch = false
|
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
intent.action = Intent.ACTION_DEFAULT
|
intent.action = Intent.ACTION_DEFAULT
|
||||||
intent.removeExtra(SearchManager.QUERY)
|
intent.removeExtra(SearchManager.QUERY)
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
|
||||||
|
class IconPickerActivity : LockingActivity() {
|
||||||
|
|
||||||
|
private lateinit var toolbar: Toolbar
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private lateinit var uploadButton: View
|
||||||
|
private var lockView: View? = null
|
||||||
|
|
||||||
|
private var mIconImage: IconImage = IconImage()
|
||||||
|
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
private val iconPickerViewModel: IconPickerViewModel by viewModels()
|
||||||
|
private var mCustomIconsSelectionMode = false
|
||||||
|
private var mIconsSelected: List<IconImageCustom> = ArrayList()
|
||||||
|
|
||||||
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_icon_picker)
|
||||||
|
|
||||||
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
toolbar.title = " "
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
updateIconsSelectedViews()
|
||||||
|
|
||||||
|
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||||
|
|
||||||
|
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||||
|
if (mDatabase?.allowCustomIcons == true) {
|
||||||
|
uploadButton.setOnClickListener {
|
||||||
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||||
|
}
|
||||||
|
uploadButton.setOnLongClickListener {
|
||||||
|
mSelectFileHelper?.selectFileOnClickViewListener?.onLongClick(it)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uploadButton.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
intent?.getParcelableExtra<IconImage>(EXTRA_ICON)?.let {
|
||||||
|
mIconImage = it
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
|
add(R.id.icon_picker_fragment, IconPickerFragment.getInstance(
|
||||||
|
// Default selection tab
|
||||||
|
if (mIconImage.custom.isUnknown)
|
||||||
|
IconPickerFragment.IconTab.STANDARD
|
||||||
|
else
|
||||||
|
IconPickerFragment.IconTab.CUSTOM
|
||||||
|
), ICON_PICKER_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus view to reinitialize timeout
|
||||||
|
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||||
|
|
||||||
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
|
|
||||||
|
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||||
|
mIconImage.standard = iconStandard
|
||||||
|
// Remove the custom icon if a standard one is selected
|
||||||
|
mIconImage.custom = IconImageCustom()
|
||||||
|
setResult()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconPicked.observe(this) { iconCustom ->
|
||||||
|
// Keep the standard icon if a custom one is selected
|
||||||
|
mIconImage.custom = iconCustom
|
||||||
|
setResult()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconsSelected.observe(this) { iconsSelected ->
|
||||||
|
mIconsSelected = iconsSelected
|
||||||
|
updateIconsSelectedViews()
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconAdded.observe(this) { iconCustomAdded ->
|
||||||
|
if (iconCustomAdded.error && !iconCustomAdded.errorConsumed) {
|
||||||
|
Snackbar.make(coordinatorLayout, iconCustomAdded.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
iconCustomAdded.errorConsumed = true
|
||||||
|
}
|
||||||
|
uploadButton.isEnabled = true
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconRemoved.observe(this) { iconCustomRemoved ->
|
||||||
|
if (iconCustomRemoved.error && !iconCustomRemoved.errorConsumed) {
|
||||||
|
Snackbar.make(coordinatorLayout, iconCustomRemoved.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
iconCustomRemoved.errorConsumed = true
|
||||||
|
}
|
||||||
|
uploadButton.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateIconsSelectedViews() {
|
||||||
|
if (mIconsSelected.isEmpty()) {
|
||||||
|
mCustomIconsSelectionMode = false
|
||||||
|
toolbar.title = " "
|
||||||
|
} else {
|
||||||
|
mCustomIconsSelectionMode = true
|
||||||
|
toolbar.title = mIconsSelected.size.toString()
|
||||||
|
}
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putParcelable(EXTRA_ICON, mIconImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Show the lock button
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding if lock button visible
|
||||||
|
toolbar.updateLockPaddingLeft()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
|
if (mCustomIconsSelectionMode) {
|
||||||
|
menuInflater.inflate(R.menu.icon, menu)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
if (mCustomIconsSelectionMode) {
|
||||||
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
|
} else {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R.id.menu_delete -> {
|
||||||
|
mIconsSelected.forEach { iconToRemove ->
|
||||||
|
removeCustomIcon(iconToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addCustomIcon(iconToUploadUri: Uri?) {
|
||||||
|
uploadButton.isEnabled = false
|
||||||
|
mainScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// on Progress with thread
|
||||||
|
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
|
||||||
|
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
|
||||||
|
UriUtil.getFileData(this@IconPickerActivity, iconToUploadUri)?.also { documentFile ->
|
||||||
|
if (documentFile.length() > MAX_ICON_SIZE) {
|
||||||
|
iconCustomState.errorStringId = R.string.error_file_to_big
|
||||||
|
} else {
|
||||||
|
mDatabase?.buildNewCustomIcon(UriUtil.getBinaryDir(this@IconPickerActivity)) { customIcon, binary ->
|
||||||
|
if (customIcon != null) {
|
||||||
|
iconCustomState.iconCustom = customIcon
|
||||||
|
BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(contentResolver,
|
||||||
|
iconToUploadUri, binary)
|
||||||
|
when {
|
||||||
|
binary == null -> {
|
||||||
|
}
|
||||||
|
binary.getSize() <= 0 -> {
|
||||||
|
}
|
||||||
|
mDatabase?.isCustomIconBinaryDuplicate(binary) == true -> {
|
||||||
|
iconCustomState.errorStringId = R.string.error_duplicate_file
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
iconCustomState.error = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (iconCustomState.error) {
|
||||||
|
mDatabase?.removeCustomIcon(customIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconCustomState
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
asyncResult.await()?.let { customIcon ->
|
||||||
|
iconPickerViewModel.addCustomIcon(customIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
||||||
|
uploadButton.isEnabled = false
|
||||||
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
|
mDatabase?.removeCustomIcon(iconImageCustom)
|
||||||
|
iconPickerViewModel.removeCustomIcon(
|
||||||
|
IconPickerViewModel.IconCustomState(iconImageCustom, false, R.string.error_remove_file)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
|
addCustomIcon(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setResult() {
|
||||||
|
setResult(Activity.RESULT_OK, Intent().apply {
|
||||||
|
putExtra(EXTRA_ICON, mIconImage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
setResult()
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 launch(context: Activity,
|
||||||
|
previousIcon: IconImage?) {
|
||||||
|
// Create an instance to return the picker icon
|
||||||
|
context.startActivityForResult(
|
||||||
|
Intent(context,
|
||||||
|
IconPickerActivity::class.java).apply {
|
||||||
|
if (previousIcon != null)
|
||||||
|
putExtra(EXTRA_ICON, previousIcon)
|
||||||
|
},
|
||||||
|
ICON_SELECTED_REQUEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import com.igreenwood.loupe.Loupe
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class ImageViewerActivity : LockingActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_image_viewer)
|
||||||
|
|
||||||
|
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container)
|
||||||
|
val imageView: ImageView = findViewById(R.id.image_viewer_image)
|
||||||
|
val progressView: View = findViewById(R.id.image_viewer_progress)
|
||||||
|
|
||||||
|
// Approximately, to not OOM and allow a zoom
|
||||||
|
val mImagePreviewMaxWidth = max(
|
||||||
|
resources.displayMetrics.widthPixels * 2,
|
||||||
|
resources.displayMetrics.heightPixels * 2
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
progressView.visibility = View.VISIBLE
|
||||||
|
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||||
|
|
||||||
|
supportActionBar?.title = attachment.name
|
||||||
|
supportActionBar?.subtitle = Formatter.formatFileSize(this, attachment.binaryData.getSize())
|
||||||
|
|
||||||
|
BinaryDatabaseManager.loadBitmap(
|
||||||
|
attachment.binaryData,
|
||||||
|
Database.getInstance().loadedCipherKey,
|
||||||
|
mImagePreviewMaxWidth
|
||||||
|
) { bitmapLoaded ->
|
||||||
|
if (bitmapLoaded == null) {
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
progressView.visibility = View.GONE
|
||||||
|
imageView.setImageBitmap(bitmapLoaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: finish()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to view the binary", e)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
Loupe.create(imageView, imageContainerView) {
|
||||||
|
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
|
||||||
|
|
||||||
|
override fun onStart(view: ImageView) {
|
||||||
|
// called when the view starts moving
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewTranslate(view: ImageView, amount: Float) {
|
||||||
|
// called whenever the view position changed
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestore(view: ImageView) {
|
||||||
|
// called when the view drag gesture ended
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(view: ImageView) {
|
||||||
|
// called when the view drag gesture ended
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> finish()
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = ImageViewerActivity::class.simpleName
|
||||||
|
|
||||||
|
private const val IMAGE_ATTACHMENT_TAG = "IMAGE_ATTACHMENT_TAG"
|
||||||
|
|
||||||
|
fun getInstance(context: Context, imageAttachment: Attachment) {
|
||||||
|
context.startActivity(Intent(context, ImageViewerActivity::class.java).apply {
|
||||||
|
putExtra(IMAGE_ATTACHMENT_TAG, imageAttachment)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import android.widget.*
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
@@ -56,14 +57,14 @@ import com.kunzisoft.keepass.database.element.Database
|
|||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
@@ -71,7 +72,6 @@ import com.kunzisoft.keepass.utils.UriUtil
|
|||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_password.*
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||||
@@ -84,8 +84,9 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
private var checkboxPasswordView: CompoundButton? = null
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
private var checkboxKeyFileView: CompoundButton? = null
|
||||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||||
|
|
||||||
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
|
||||||
@@ -131,6 +132,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
@@ -236,15 +238,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
showLoadDatabaseDuplicateUuidMessage {
|
showLoadDatabaseDuplicateUuidMessage {
|
||||||
|
|
||||||
var databaseUri: Uri? = null
|
var databaseUri: Uri? = null
|
||||||
var masterPassword: String? = null
|
var mainCredential: MainCredential = MainCredential()
|
||||||
var keyFileUri: Uri? = null
|
|
||||||
var readOnly = true
|
var readOnly = true
|
||||||
var cipherEntity: CipherDatabaseEntity? = null
|
var cipherEntity: CipherDatabaseEntity? = null
|
||||||
|
|
||||||
result.data?.let { resultData ->
|
result.data?.let { resultData ->
|
||||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
|
||||||
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
|
|
||||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||||
}
|
}
|
||||||
@@ -252,8 +252,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
databaseUri?.let { databaseFileUri ->
|
databaseUri?.let { databaseFileUri ->
|
||||||
showProgressDialogAndLoadDatabase(
|
showProgressDialogAndLoadDatabase(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
masterPassword,
|
mainCredential,
|
||||||
keyFileUri,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
cipherEntity,
|
cipherEntity,
|
||||||
true)
|
true)
|
||||||
@@ -274,7 +273,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
resultError = "$resultError $resultMessage"
|
resultError = "$resultError $resultMessage"
|
||||||
}
|
}
|
||||||
Log.e(TAG, resultError)
|
Log.e(TAG, resultError)
|
||||||
Snackbar.make(activity_password_coordinator_layout,
|
Snackbar.make(coordinatorLayout,
|
||||||
resultError,
|
resultError,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
@@ -526,7 +525,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
) {
|
) {
|
||||||
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
||||||
Snackbar.make(activity_password_coordinator_layout,
|
Snackbar.make(coordinatorLayout,
|
||||||
R.string.autofill_read_only_save,
|
R.string.autofill_read_only_save,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
} else {
|
} else {
|
||||||
@@ -534,8 +533,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
// Show the progress dialog and load the database
|
// Show the progress dialog and load the database
|
||||||
showProgressDialogAndLoadDatabase(
|
showProgressDialogAndLoadDatabase(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
password,
|
MainCredential(password, keyFileUri),
|
||||||
keyFileUri,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
cipherDatabaseEntity,
|
cipherDatabaseEntity,
|
||||||
false)
|
false)
|
||||||
@@ -544,15 +542,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||||
password: String?,
|
mainCredential: MainCredential,
|
||||||
keyFile: Uri?,
|
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
password,
|
mainCredential,
|
||||||
keyFile,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
cipherDatabaseEntity,
|
cipherDatabaseEntity,
|
||||||
fixDuplicateUUID
|
fixDuplicateUUID
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import androidx.fragment.app.DialogFragment
|
|||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
|
||||||
@@ -76,10 +77,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AssignPasswordDialogListener {
|
interface AssignPasswordDialogListener {
|
||||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
||||||
fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(activity: Context) {
|
override fun onAttach(activity: Context) {
|
||||||
@@ -121,8 +120,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
|
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||||
credentialsInfo?.setOnClickListener {
|
|
||||||
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,17 +159,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!error) {
|
if (!error) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||||
negativeButton.setOnClickListener {
|
negativeButton.setOnClickListener {
|
||||||
mListener?.onAssignKeyDialogNegativeClick(
|
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,6 +177,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun retrieveMainCredential(): MainCredential {
|
||||||
|
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
|
||||||
|
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
|
||||||
|
return MainCredential(masterPassword, keyFile)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
@@ -242,9 +242,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
builder.setMessage(R.string.warning_empty_password)
|
builder.setMessage(R.string.warning_empty_password)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (!verifyKeyFile()) {
|
if (!verifyKeyFile()) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,9 +257,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
builder.setMessage(R.string.warning_no_encryption_key)
|
builder.setMessage(R.string.warning_no_encryption_key)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ import androidx.fragment.app.DialogFragment
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
|
|
||||||
open class DeleteNodesDialogFragment : DialogFragment() {
|
open class DeleteNodesDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class DuplicateUuidDialog : DialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply {
|
val builder = AlertDialog.Builder(activity).apply {
|
||||||
val message = getString(R.string.contains_duplicate_uuid) +
|
val message = getString(R.string.contains_duplicate_uuid) +
|
||||||
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
||||||
setMessage(message)
|
setMessage(message)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
|
|
||||||
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
|
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
|
||||||
|
|
||||||
|
|||||||
@@ -23,34 +23,40 @@ import android.app.Dialog
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import android.view.View
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.IconPickerActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
|
import com.kunzisoft.keepass.view.ExpirationView
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
class GroupEditDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private var editGroupListener: EditGroupListener? = null
|
private var mEditGroupListener: EditGroupListener? = null
|
||||||
|
|
||||||
private var editGroupDialogAction: EditGroupDialogAction? = null
|
private var mEditGroupDialogAction = EditGroupDialogAction.NONE
|
||||||
private var nameGroup: String? = null
|
private var mGroupInfo = GroupInfo()
|
||||||
private var iconGroup: IconImage? = null
|
|
||||||
|
|
||||||
private var nameTextLayoutView: TextInputLayout? = null
|
private lateinit var iconButtonView: ImageView
|
||||||
private var nameTextView: TextView? = null
|
|
||||||
private var iconButtonView: ImageView? = null
|
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
|
private lateinit var nameTextLayoutView: TextInputLayout
|
||||||
|
private lateinit var nameTextView: TextView
|
||||||
|
private lateinit var notesTextLayoutView: TextInputLayout
|
||||||
|
private lateinit var notesTextView: TextView
|
||||||
|
private lateinit var expirationView: ExpirationView
|
||||||
|
|
||||||
enum class EditGroupDialogAction {
|
enum class EditGroupDialogAction {
|
||||||
CREATION, UPDATE, NONE;
|
CREATION, UPDATE, NONE;
|
||||||
@@ -67,7 +73,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
// Verify that the host activity implements the callback interface
|
// Verify that the host activity implements the callback interface
|
||||||
try {
|
try {
|
||||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
// Instantiate the NoticeDialogListener so we can send events to the host
|
||||||
editGroupListener = context as EditGroupListener
|
mEditGroupListener = context as EditGroupListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
// The activity doesn't implement the interface, throw exception
|
// The activity doesn't implement the interface, throw exception
|
||||||
throw ClassCastException(context.toString()
|
throw ClassCastException(context.toString()
|
||||||
@@ -76,16 +82,19 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetach() {
|
override fun onDetach() {
|
||||||
editGroupListener = null
|
mEditGroupListener = null
|
||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
|
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
|
||||||
nameTextLayoutView = root?.findViewById(R.id.group_edit_name_container)
|
iconButtonView = root.findViewById(R.id.group_edit_icon_button)
|
||||||
nameTextView = root?.findViewById(R.id.group_edit_name)
|
nameTextLayoutView = root.findViewById(R.id.group_edit_name_container)
|
||||||
iconButtonView = root?.findViewById(R.id.group_edit_icon_button)
|
nameTextView = root.findViewById(R.id.group_edit_name)
|
||||||
|
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
||||||
|
notesTextView = root.findViewById(R.id.group_edit_note)
|
||||||
|
expirationView = root.findViewById(R.id.group_edit_expiration)
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
@@ -94,47 +103,47 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
|
|
||||||
// Init elements
|
// Init elements
|
||||||
mDatabase = Database.getInstance()
|
mDatabase = Database.getInstance()
|
||||||
editGroupDialogAction = EditGroupDialogAction.NONE
|
|
||||||
nameGroup = ""
|
|
||||||
iconGroup = mDatabase?.iconFactory?.folderIcon
|
|
||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||||
&& savedInstanceState.containsKey(KEY_NAME)
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
&& savedInstanceState.containsKey(KEY_ICON)) {
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
||||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
nameGroup = savedInstanceState.getString(KEY_NAME)
|
|
||||||
iconGroup = savedInstanceState.getParcelable(KEY_ICON)
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_ACTION_ID))
|
if (containsKey(KEY_ACTION_ID))
|
||||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
||||||
|
if (containsKey(KEY_GROUP_INFO)) {
|
||||||
if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) {
|
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
nameGroup = getString(KEY_NAME)
|
|
||||||
iconGroup = getParcelable(KEY_ICON)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// populate the name
|
// populate info in views
|
||||||
nameTextView?.text = nameGroup
|
populateInfoToViews()
|
||||||
// populate the icon
|
expirationView.setOnDateClickListener = {
|
||||||
assignIconView()
|
expirationView.expiryTime.date.let { expiresDate ->
|
||||||
|
val dateTime = DateTime(expiresDate)
|
||||||
|
val defaultYear = dateTime.year
|
||||||
|
val defaultMonth = dateTime.monthOfYear-1
|
||||||
|
val defaultDay = dateTime.dayOfMonth
|
||||||
|
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||||
|
.show(parentFragmentManager, "DatePickerFragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setView(root)
|
builder.setView(root)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
editGroupListener?.cancelEditGroup(
|
retrieveGroupInfoFromViews()
|
||||||
editGroupDialogAction,
|
mEditGroupListener?.cancelEditGroup(
|
||||||
nameTextView?.text?.toString(),
|
mEditGroupDialogAction,
|
||||||
iconGroup)
|
mGroupInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
iconButtonView?.setOnClickListener { _ ->
|
iconButtonView.setOnClickListener { _ ->
|
||||||
IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment")
|
IconPickerActivity.launch(activity, mGroupInfo.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -150,69 +159,99 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
if (d != null) {
|
if (d != null) {
|
||||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||||
positiveButton.setOnClickListener {
|
positiveButton.setOnClickListener {
|
||||||
|
retrieveGroupInfoFromViews()
|
||||||
if (isValid()) {
|
if (isValid()) {
|
||||||
editGroupListener?.approveEditGroup(
|
mEditGroupListener?.approveEditGroup(
|
||||||
editGroupDialogAction,
|
mEditGroupDialogAction,
|
||||||
nameTextView?.text?.toString(),
|
mGroupInfo)
|
||||||
iconGroup)
|
|
||||||
d.dismiss()
|
d.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignIconView() {
|
fun getExpiryTime(): DateInstant {
|
||||||
if (mDatabase?.drawFactory != null && iconGroup != null) {
|
retrieveGroupInfoFromViews()
|
||||||
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
|
return mGroupInfo.expiryTime
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iconPicked(bundle: Bundle) {
|
fun setExpiryTime(expiryTime: DateInstant) {
|
||||||
iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle)
|
mGroupInfo.expiryTime = expiryTime
|
||||||
|
populateInfoToViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateInfoToViews() {
|
||||||
|
assignIconView()
|
||||||
|
nameTextView.text = mGroupInfo.title
|
||||||
|
notesTextLayoutView.visibility = if (mGroupInfo.notes == null) View.GONE else View.VISIBLE
|
||||||
|
mGroupInfo.notes?.let {
|
||||||
|
notesTextView.text = it
|
||||||
|
}
|
||||||
|
expirationView.expires = mGroupInfo.expires
|
||||||
|
expirationView.expiryTime = mGroupInfo.expiryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveGroupInfoFromViews() {
|
||||||
|
mGroupInfo.title = nameTextView.text.toString()
|
||||||
|
// Only if there
|
||||||
|
val newNotes = notesTextView.text.toString()
|
||||||
|
if (newNotes.isNotEmpty()) {
|
||||||
|
mGroupInfo.notes = newNotes
|
||||||
|
}
|
||||||
|
mGroupInfo.expires = expirationView.expires
|
||||||
|
mGroupInfo.expiryTime = expirationView.expiryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignIconView() {
|
||||||
|
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIcon(icon: IconImage) {
|
||||||
|
mGroupInfo.icon = icon
|
||||||
assignIconView()
|
assignIconView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal)
|
retrieveGroupInfoFromViews()
|
||||||
outState.putString(KEY_NAME, nameGroup)
|
outState.putInt(KEY_ACTION_ID, mEditGroupDialogAction.ordinal)
|
||||||
outState.putParcelable(KEY_ICON, iconGroup)
|
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isValid(): Boolean {
|
private fun isValid(): Boolean {
|
||||||
if (nameTextView?.text?.toString()?.isNotEmpty() != true) {
|
if (nameTextView.text.toString().isEmpty()) {
|
||||||
nameTextLayoutView?.error = getString(R.string.error_no_name)
|
nameTextLayoutView.error = getString(R.string.error_no_name)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditGroupListener {
|
interface EditGroupListener {
|
||||||
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
|
fun approveEditGroup(action: EditGroupDialogAction,
|
||||||
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
|
groupInfo: GroupInfo)
|
||||||
|
fun cancelEditGroup(action: EditGroupDialogAction,
|
||||||
|
groupInfo: GroupInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||||
|
|
||||||
const val KEY_NAME = "KEY_NAME"
|
|
||||||
const val KEY_ICON = "KEY_ICON"
|
|
||||||
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||||
|
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||||
|
|
||||||
fun build(): GroupEditDialogFragment {
|
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
|
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
val fragment = GroupEditDialogFragment()
|
val fragment = GroupEditDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(group: Group): GroupEditDialogFragment {
|
fun update(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putString(KEY_NAME, group.title)
|
|
||||||
bundle.putParcelable(KEY_ICON, group.icon)
|
|
||||||
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
|
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
val fragment = GroupEditDialogFragment()
|
val fragment = GroupEditDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.content.Context
|
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.BaseAdapter
|
|
||||||
import android.widget.GridView
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.icons.IconPack
|
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
|
||||||
|
|
||||||
|
|
||||||
class IconPickerDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var iconPickerListener: IconPickerListener? = null
|
|
||||||
private var iconPack: IconPack? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
iconPickerListener = context as IconPickerListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
// The activity doesn't implement the interface, throw exception
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + IconPickerListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
iconPickerListener = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
|
|
||||||
iconPack = IconPackChooser.getSelectedIconPack(requireContext())
|
|
||||||
|
|
||||||
// Inflate and set the layout for the dialog
|
|
||||||
// Pass null as the parent view because its going in the dialog layout
|
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_picker, null)
|
|
||||||
builder.setView(root)
|
|
||||||
|
|
||||||
val currIconGridView = root.findViewById<GridView>(R.id.IconGridView)
|
|
||||||
currIconGridView.adapter = ImageAdapter(activity)
|
|
||||||
|
|
||||||
currIconGridView.setOnItemClickListener { _, _, position, _ ->
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putParcelable(KEY_ICON_STANDARD, IconImageStandard(position))
|
|
||||||
iconPickerListener?.iconPicked(bundle)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() {
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return iconPack?.numberOfIcons() ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Any? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val currentView: View = convertView
|
|
||||||
?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
|
|
||||||
.inflate(R.layout.item_icon, parent, false)
|
|
||||||
|
|
||||||
iconPack?.let { iconPack ->
|
|
||||||
val iconImageView = currentView.findViewById<ImageView>(R.id.icon_image)
|
|
||||||
iconImageView.setImageResource(iconPack.iconToResId(position))
|
|
||||||
|
|
||||||
// Assign color if icons are tintable
|
|
||||||
if (iconPack.tintable()) {
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
|
||||||
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
|
||||||
ta.recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IconPickerListener {
|
|
||||||
fun iconPicked(bundle: Bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
|
|
||||||
|
|
||||||
fun getIconStandardFromBundle(bundle: Bundle): IconImageStandard? {
|
|
||||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launch(activity: FragmentActivity) {
|
|
||||||
// Create an instance of the dialog fragment and show it
|
|
||||||
val dialog = IconPickerDialogFragment()
|
|
||||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
|
||||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -49,10 +50,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||||
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
|
val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
|
||||||
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
|
|
||||||
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
|
|
||||||
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
|
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -60,10 +58,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.onPasswordEncodingValidateListener(
|
mListener?.onPasswordEncodingValidateListener(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPasswordChecked,
|
mainCredential
|
||||||
masterPassword,
|
|
||||||
keyFileChecked,
|
|
||||||
keyFile
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||||
@@ -75,32 +70,20 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
masterPasswordChecked: Boolean,
|
mainCredential: MainCredential)
|
||||||
masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean,
|
|
||||||
keyFile: Uri?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
|
||||||
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
|
||||||
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
|
||||||
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
|
|
||||||
|
|
||||||
fun getInstance(databaseUri: Uri,
|
fun getInstance(databaseUri: Uri,
|
||||||
masterPasswordChecked: Boolean,
|
mainCredential: MainCredential): SortDialogFragment {
|
||||||
masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean,
|
|
||||||
keyFile: Uri?): SortDialogFragment {
|
|
||||||
val fragment = SortDialogFragment()
|
val fragment = SortDialogFragment()
|
||||||
fragment.arguments = Bundle().apply {
|
fragment.arguments = Bundle().apply {
|
||||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putParcelable(MAIN_CREDENTIAL, mainCredential)
|
||||||
putString(MASTER_PASSWORD_KEY, masterPassword)
|
|
||||||
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
|
|
||||||
putParcelable(KEY_FILE_URI_KEY, keyFile)
|
|
||||||
}
|
}
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
|||||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SetOTPDialogFragment : DialogFragment() {
|
class SetOTPDialogFragment : DialogFragment() {
|
||||||
@@ -223,13 +224,16 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.apply {
|
builder.apply {
|
||||||
setTitle(R.string.entry_setup_otp)
|
|
||||||
setView(root)
|
setView(root)
|
||||||
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
|
||||||
|
UriUtil.gotoUrl(activity, R.string.otp_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
@@ -26,29 +26,29 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.model.*
|
import com.kunzisoft.keepass.model.*
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.ExpirationView
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
import com.kunzisoft.keepass.view.collapse
|
import com.kunzisoft.keepass.view.collapse
|
||||||
import com.kunzisoft.keepass.view.expand
|
import com.kunzisoft.keepass.view.expand
|
||||||
@@ -63,8 +63,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
private lateinit var entryPasswordLayoutView: TextInputLayout
|
private lateinit var entryPasswordLayoutView: TextInputLayout
|
||||||
private lateinit var entryPasswordView: EditText
|
private lateinit var entryPasswordView: EditText
|
||||||
private lateinit var entryPasswordGeneratorView: View
|
private lateinit var entryPasswordGeneratorView: View
|
||||||
private lateinit var entryExpiresCheckBox: CompoundButton
|
private lateinit var entryExpirationView: ExpirationView
|
||||||
private lateinit var entryExpiresTextView: TextView
|
|
||||||
private lateinit var entryNotesView: EditText
|
private lateinit var entryNotesView: EditText
|
||||||
private lateinit var extraFieldsContainerView: View
|
private lateinit var extraFieldsContainerView: View
|
||||||
private lateinit var extraFieldsListView: ViewGroup
|
private lateinit var extraFieldsListView: ViewGroup
|
||||||
@@ -75,17 +74,17 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
|
|
||||||
private var fontInVisibility: Boolean = false
|
private var fontInVisibility: Boolean = false
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
|
|
||||||
|
|
||||||
var drawFactory: IconDrawableFactory? = null
|
var drawFactory: IconDrawableFactory? = null
|
||||||
var setOnDateClickListener: View.OnClickListener? = null
|
var setOnDateClickListener: (() -> Unit)? = null
|
||||||
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
||||||
var setOnIconViewClickListener: View.OnClickListener? = null
|
var setOnIconViewClickListener: ((IconImage) -> Unit)? = null
|
||||||
var setOnEditCustomField: ((Field) -> Unit)? = null
|
var setOnEditCustomField: ((Field) -> Unit)? = null
|
||||||
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
||||||
|
|
||||||
// Elements to modify the current entry
|
// Elements to modify the current entry
|
||||||
private var mEntryInfo = EntryInfo()
|
private var mEntryInfo = EntryInfo()
|
||||||
|
private var mBinaryCipherKey: Database.LoadedKey? = null
|
||||||
private var mLastFocusedEditField: FocusedEditField? = null
|
private var mLastFocusedEditField: FocusedEditField? = null
|
||||||
private var mExtraViewToRequestFocus: EditText? = null
|
private var mExtraViewToRequestFocus: EditText? = null
|
||||||
|
|
||||||
@@ -101,7 +100,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
||||||
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
||||||
entryIconView.setOnClickListener {
|
entryIconView.setOnClickListener {
|
||||||
setOnIconViewClickListener?.onClick(it)
|
setOnIconViewClickListener?.invoke(mEntryInfo.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
||||||
@@ -112,12 +111,8 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
entryPasswordGeneratorView.setOnClickListener {
|
entryPasswordGeneratorView.setOnClickListener {
|
||||||
setOnPasswordGeneratorClickListener?.onClick(it)
|
setOnPasswordGeneratorClickListener?.onClick(it)
|
||||||
}
|
}
|
||||||
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
|
entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration)
|
||||||
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
|
entryExpirationView.setOnDateClickListener = setOnDateClickListener
|
||||||
entryExpiresTextView.setOnClickListener {
|
|
||||||
if (entryExpiresCheckBox.isChecked)
|
|
||||||
setOnDateClickListener?.onClick(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
||||||
|
|
||||||
@@ -127,6 +122,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||||
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||||
|
attachmentsAdapter.binaryCipherKey = arguments?.getSerializable(KEY_BINARY_CIPHER_KEY) as? Database.LoadedKey?
|
||||||
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
if (previousSize > 0 && newSize == 0) {
|
if (previousSize > 0 && newSize == 0) {
|
||||||
attachmentsContainerView.collapse(true)
|
attachmentsContainerView.collapse(true)
|
||||||
@@ -140,10 +136,6 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
}
|
}
|
||||||
|
|
||||||
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
|
||||||
assignExpiresDateText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
||||||
@@ -178,7 +170,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
setOnEditCustomField = null
|
setOnEditCustomField = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryInfo(): EntryInfo? {
|
fun getEntryInfo(): EntryInfo {
|
||||||
populateEntryWithViews()
|
populateEntryWithViews()
|
||||||
return mEntryInfo
|
return mEntryInfo
|
||||||
}
|
}
|
||||||
@@ -247,9 +239,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
mEntryInfo.icon = value
|
mEntryInfo.icon = value
|
||||||
drawFactory?.let { drawFactory ->
|
drawFactory?.assignDatabaseIcon(entryIconView, value, iconColor)
|
||||||
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var username: String
|
var username: String
|
||||||
@@ -283,41 +273,20 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignExpiresDateText() {
|
|
||||||
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
|
||||||
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
|
|
||||||
expiresInstant.getDateTimeString(resources)
|
|
||||||
} else {
|
|
||||||
entryExpiresTextView.setOnClickListener(null)
|
|
||||||
resources.getString(R.string.never)
|
|
||||||
}
|
|
||||||
if (fontInVisibility)
|
|
||||||
entryExpiresTextView.applyFontVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
var expires: Boolean
|
var expires: Boolean
|
||||||
get() {
|
get() {
|
||||||
return entryExpiresCheckBox.isChecked
|
return entryExpirationView.expires
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
if (!value) {
|
entryExpirationView.expires = value
|
||||||
expiresInstant = DateInstant.IN_ONE_MONTH
|
|
||||||
}
|
|
||||||
entryExpiresCheckBox.isChecked = value
|
|
||||||
assignExpiresDateText()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var expiryTime: DateInstant
|
var expiryTime: DateInstant
|
||||||
get() {
|
get() {
|
||||||
return if (expires)
|
return entryExpirationView.expiryTime
|
||||||
expiresInstant
|
|
||||||
else
|
|
||||||
DateInstant.NEVER_EXPIRE
|
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
if (expires)
|
entryExpirationView.expiryTime = value
|
||||||
expiresInstant = value
|
|
||||||
assignExpiresDateText()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var notes: String
|
var notes: String
|
||||||
@@ -344,7 +313,8 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
itemView?.id = View.NO_ID
|
itemView?.id = View.NO_ID
|
||||||
|
|
||||||
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||||
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
|
extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected)
|
||||||
|
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
|
||||||
extraFieldValueContainer?.hint = extraField.name
|
extraFieldValueContainer?.hint = extraField.name
|
||||||
extraFieldValueContainer?.id = View.NO_ID
|
extraFieldValueContainer?.id = View.NO_ID
|
||||||
|
|
||||||
@@ -502,9 +472,13 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
return attachmentsAdapter.contains(attachment)
|
return attachmentsAdapter.contains(attachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putAttachment(attachment: EntryAttachmentState) {
|
fun putAttachment(attachment: EntryAttachmentState,
|
||||||
|
onPreviewLoaded: (()-> Unit)? = null) {
|
||||||
attachmentsContainerView.visibility = View.VISIBLE
|
attachmentsContainerView.visibility = View.VISIBLE
|
||||||
attachmentsAdapter.putItem(attachment)
|
attachmentsAdapter.putItem(attachment)
|
||||||
|
attachmentsAdapter.onBinaryPreviewLoaded = {
|
||||||
|
onPreviewLoaded?.invoke()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachment(attachment: EntryAttachmentState) {
|
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||||
@@ -528,6 +502,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
populateEntryWithViews()
|
populateEntryWithViews()
|
||||||
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||||
|
outState.putSerializable(KEY_BINARY_CIPHER_KEY, mBinaryCipherKey)
|
||||||
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
||||||
|
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
@@ -535,12 +510,15 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||||
|
const val KEY_BINARY_CIPHER_KEY = "KEY_BINARY_CIPHER_KEY"
|
||||||
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||||
|
|
||||||
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
fun getInstance(entryInfo: EntryInfo?,
|
||||||
|
loadedKey: Database.LoadedKey?): EntryEditFragment {
|
||||||
return EntryEditFragment().apply {
|
return EntryEditFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||||
|
putSerializable(KEY_BINARY_CIPHER_KEY, loadedKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* 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.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
|
||||||
|
|
||||||
|
class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||||
|
|
||||||
|
override fun retrieveMainLayoutId(): Int {
|
||||||
|
return R.layout.fragment_icon_grid
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun defineIconList() {
|
||||||
|
mDatabase?.doForEachCustomIcons { customIcon, _ ->
|
||||||
|
iconPickerAdapter.addIcon(customIcon, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
iconPickerViewModel.customIconsSelected.observe(viewLifecycleOwner) { customIconsSelected ->
|
||||||
|
if (customIconsSelected.isEmpty()) {
|
||||||
|
iconActionSelectionMode = false
|
||||||
|
iconPickerAdapter.deselectAllIcons()
|
||||||
|
} else {
|
||||||
|
iconActionSelectionMode = true
|
||||||
|
iconPickerAdapter.updateIconSelectedState(customIconsSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { iconCustomAdded ->
|
||||||
|
if (!iconCustomAdded.error) {
|
||||||
|
iconCustomAdded?.iconCustom?.let { icon ->
|
||||||
|
iconPickerAdapter.addIcon(icon)
|
||||||
|
iconCustomAdded.iconCustom = null
|
||||||
|
}
|
||||||
|
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
|
||||||
|
if (!iconCustomRemoved.error) {
|
||||||
|
iconCustomRemoved?.iconCustom?.let { icon ->
|
||||||
|
iconPickerAdapter.removeIcon(icon)
|
||||||
|
iconCustomRemoved.iconCustom = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIconClickListener(icon: IconImageCustom) {
|
||||||
|
if (iconActionSelectionMode) {
|
||||||
|
// Same long click behavior after each single click
|
||||||
|
onIconLongClickListener(icon)
|
||||||
|
} else {
|
||||||
|
iconPickerViewModel.pickCustomIcon(icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIconLongClickListener(icon: IconImageCustom) {
|
||||||
|
// Select or deselect item if already selected
|
||||||
|
icon.selected = !icon.selected
|
||||||
|
iconPickerAdapter.updateIcon(icon)
|
||||||
|
iconActionSelectionMode = iconPickerAdapter.containsAnySelectedIcon()
|
||||||
|
iconPickerViewModel.selectCustomIcons(iconPickerAdapter.getSelectedIcons())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* 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.fragments
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.adapters.IconPickerAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||||
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
|
||||||
|
IconPickerAdapter.IconPickerListener<T> {
|
||||||
|
|
||||||
|
protected lateinit var iconsGridView: RecyclerView
|
||||||
|
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
|
||||||
|
protected var iconActionSelectionMode = false
|
||||||
|
|
||||||
|
protected var mDatabase: Database? = null
|
||||||
|
|
||||||
|
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
|
abstract fun retrieveMainLayoutId(): Int
|
||||||
|
|
||||||
|
abstract fun defineIconList()
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
|
||||||
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
|
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||||
|
ta?.recycle()
|
||||||
|
|
||||||
|
iconPickerAdapter = IconPickerAdapter<T>(context, tintColor).apply {
|
||||||
|
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val populateList = launch {
|
||||||
|
iconPickerAdapter.clear()
|
||||||
|
defineIconList()
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
populateList.join()
|
||||||
|
iconPickerAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View {
|
||||||
|
val root = inflater.inflate(retrieveMainLayoutId(), container, false)
|
||||||
|
iconsGridView = root.findViewById(R.id.icons_grid_view)
|
||||||
|
iconsGridView.adapter = iconPickerAdapter
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
iconPickerAdapter.iconPickerListener = this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIconDeleteClicked() {
|
||||||
|
iconActionSelectionMode = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
|
||||||
|
class IconPickerFragment : StylishFragment() {
|
||||||
|
|
||||||
|
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
|
||||||
|
private lateinit var viewPager: ViewPager2
|
||||||
|
|
||||||
|
private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_icon_picker, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
|
viewPager = view.findViewById(R.id.icon_picker_pager)
|
||||||
|
val tabLayout = view.findViewById<TabLayout>(R.id.icon_picker_tabs)
|
||||||
|
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||||
|
if (mDatabase?.allowCustomIcons == true) 2 else 1)
|
||||||
|
viewPager.adapter = iconPickerPagerAdapter
|
||||||
|
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||||
|
tab.text = when (position) {
|
||||||
|
1 -> getString(R.string.icon_section_custom)
|
||||||
|
else -> getString(R.string.icon_section_standard)
|
||||||
|
}
|
||||||
|
}.attach()
|
||||||
|
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(ICON_TAB_ARG)) {
|
||||||
|
viewPager.currentItem = getInt(ICON_TAB_ARG)
|
||||||
|
}
|
||||||
|
remove(ICON_TAB_ARG)
|
||||||
|
}
|
||||||
|
|
||||||
|
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { _ ->
|
||||||
|
viewPager.currentItem = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class IconTab {
|
||||||
|
STANDARD, CUSTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val ICON_TAB_ARG = "ICON_TAB_ARG"
|
||||||
|
|
||||||
|
fun getInstance(iconTab: IconTab): IconPickerFragment {
|
||||||
|
val fragment = IconPickerFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putInt(ICON_TAB_ARG, iconTab.ordinal)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.fragments
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
|
||||||
|
|
||||||
|
class IconStandardFragment : IconFragment<IconImageStandard>() {
|
||||||
|
|
||||||
|
override fun retrieveMainLayoutId(): Int {
|
||||||
|
return R.layout.fragment_icon_grid
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun defineIconList() {
|
||||||
|
mDatabase?.doForEachStandardIcons { standardIcon ->
|
||||||
|
iconPickerAdapter.addIcon(standardIcon, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIconClickListener(icon: IconImageStandard) {
|
||||||
|
iconPickerViewModel.pickStandardIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIconLongClickListener(icon: IconImageStandard) {}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -28,6 +28,7 @@ import androidx.appcompat.view.ActionMode
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
package com.kunzisoft.keepass.activities.stylish
|
package com.kunzisoft.keepass.activities.stylish
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.StyleRes
|
import android.content.res.Configuration
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that provides functions to retrieve and assign a theme to a module
|
* Class that provides functions to retrieve and assign a theme to a module
|
||||||
@@ -38,17 +38,58 @@ object Stylish {
|
|||||||
* @param context Context to retrieve the theme preference
|
* @param context Context to retrieve the theme preference
|
||||||
*/
|
*/
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
val stylishPrefKey = context.getString(R.string.setting_style_key)
|
|
||||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||||
themeString = PreferenceManager.getDefaultSharedPreferences(context).getString(stylishPrefKey, context.getString(R.string.list_style_name_light))
|
themeString = PreferencesUtil.getStyle(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
||||||
|
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
||||||
|
context.getString(R.string.list_style_brightness_light) -> false
|
||||||
|
context.getString(R.string.list_style_brightness_night) -> true
|
||||||
|
else -> {
|
||||||
|
when (context.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) {
|
||||||
|
Configuration.UI_MODE_NIGHT_YES -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (systemNightMode) {
|
||||||
|
retrieveEquivalentNightStyle(context, styleString)
|
||||||
|
} else {
|
||||||
|
retrieveEquivalentLightStyle(context, styleString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retrieveEquivalentLightStyle(context: Context, styleString: String): String {
|
||||||
|
return when (styleString) {
|
||||||
|
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
|
||||||
|
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
|
||||||
|
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
|
||||||
|
context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue)
|
||||||
|
context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red)
|
||||||
|
context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple)
|
||||||
|
else -> styleString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveEquivalentNightStyle(context: Context, styleString: String): String {
|
||||||
|
return when (styleString) {
|
||||||
|
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
|
||||||
|
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
|
||||||
|
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
|
||||||
|
context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night)
|
||||||
|
context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night)
|
||||||
|
context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark)
|
||||||
|
else -> styleString
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign the style to the class attribute
|
* Assign the style to the class attribute
|
||||||
* @param styleString Style id String
|
* @param styleString Style id String
|
||||||
*/
|
*/
|
||||||
fun assignStyle(styleString: String) {
|
fun assignStyle(context: Context, styleString: String) {
|
||||||
themeString = styleString
|
themeString = retrieveEquivalentSystemStyle(context, styleString)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,14 +99,18 @@ object Stylish {
|
|||||||
*/
|
*/
|
||||||
@StyleRes
|
@StyleRes
|
||||||
fun getThemeId(context: Context): Int {
|
fun getThemeId(context: Context): Int {
|
||||||
|
return when (retrieveEquivalentSystemStyle(context, themeString ?: context.getString(R.string.list_style_name_light))) {
|
||||||
return when (themeString) {
|
|
||||||
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
||||||
|
context.getString(R.string.list_style_name_white) -> R.style.KeepassDXStyle_White
|
||||||
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
||||||
|
context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear
|
||||||
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
|
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
|
||||||
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
|
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
|
||||||
|
context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night
|
||||||
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
|
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
|
||||||
|
context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night
|
||||||
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
||||||
|
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
|
||||||
else -> R.style.KeepassDXStyle_Light
|
else -> R.style.KeepassDXStyle_Light
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ abstract class StylishFragment : Fragment() {
|
|||||||
contextThemed = ContextThemeWrapper(context, themeId)
|
contextThemed = ContextThemeWrapper(context, themeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
// To fix status bar color
|
// To fix status bar color
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
@@ -53,14 +54,21 @@ abstract class StylishFragment : Fragment() {
|
|||||||
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
taStatusBarColor?.recycle()
|
taStatusBarColor?.recycle()
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
try {
|
||||||
|
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
||||||
|
if (taWindowStatusLight?.getBoolean(0, false) == true) {
|
||||||
|
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||||
|
}
|
||||||
|
taWindowStatusLight?.recycle()
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
||||||
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
taNavigationBarColor?.recycle()
|
taNavigationBarColor?.recycle()
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onCreateView(inflater, container, savedInstanceState)
|
return super.onCreateView(inflater, container, savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
itemsList.clear()
|
if (itemsList.size > 0) {
|
||||||
notifyDataSetChanged()
|
itemsList.clear()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,17 +31,29 @@ import android.widget.ProgressBar
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.ImageViewerActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
||||||
class EntryAttachmentsItemsAdapter(context: Context)
|
class EntryAttachmentsItemsAdapter(context: Context)
|
||||||
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||||
|
|
||||||
|
var binaryCipherKey: Database.LoadedKey? = null
|
||||||
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||||
|
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
|
||||||
|
|
||||||
|
// Approximately
|
||||||
|
private val mImagePreviewMaxWidth = max(
|
||||||
|
context.resources.displayMetrics.widthPixels,
|
||||||
|
context.resources.getDimensionPixelSize(R.dimen.item_file_info_height)
|
||||||
|
)
|
||||||
private var mTitleColor: Int
|
private var mTitleColor: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -62,24 +74,59 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
val entryAttachmentState = itemsList[position]
|
val entryAttachmentState = itemsList[position]
|
||||||
|
|
||||||
holder.itemView.visibility = View.VISIBLE
|
holder.itemView.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileThumbnail.apply {
|
||||||
|
// Perform image loading only if upload is finished
|
||||||
|
if (entryAttachmentState.downloadState != AttachmentState.START
|
||||||
|
&& entryAttachmentState.downloadState != AttachmentState.IN_PROGRESS) {
|
||||||
|
// Show the bitmap image if loaded
|
||||||
|
if (entryAttachmentState.previewState == AttachmentState.NULL) {
|
||||||
|
entryAttachmentState.previewState = AttachmentState.IN_PROGRESS
|
||||||
|
// Load the bitmap image
|
||||||
|
BinaryDatabaseManager.loadBitmap(
|
||||||
|
entryAttachmentState.attachment.binaryData,
|
||||||
|
binaryCipherKey,
|
||||||
|
mImagePreviewMaxWidth
|
||||||
|
) { imageLoaded ->
|
||||||
|
if (imageLoaded == null) {
|
||||||
|
entryAttachmentState.previewState = AttachmentState.ERROR
|
||||||
|
visibility = View.GONE
|
||||||
|
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
|
||||||
|
} else {
|
||||||
|
entryAttachmentState.previewState = AttachmentState.COMPLETE
|
||||||
|
setImageBitmap(imageLoaded)
|
||||||
|
if (visibility != View.VISIBLE) {
|
||||||
|
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height)) {
|
||||||
|
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
this.setOnClickListener {
|
||||||
|
ImageViewerActivity.getInstance(context, entryAttachmentState.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
holder.binaryFileBroken.apply {
|
holder.binaryFileBroken.apply {
|
||||||
setColorFilter(Color.RED)
|
setColorFilter(Color.RED)
|
||||||
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
visibility = if (entryAttachmentState.attachment.binaryData.isCorrupted) {
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
||||||
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
if (entryAttachmentState.attachment.binaryData.isCorrupted) {
|
||||||
holder.binaryFileTitle.setTextColor(Color.RED)
|
holder.binaryFileTitle.setTextColor(Color.RED)
|
||||||
} else {
|
} else {
|
||||||
holder.binaryFileTitle.setTextColor(mTitleColor)
|
holder.binaryFileTitle.setTextColor(mTitleColor)
|
||||||
}
|
}
|
||||||
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
||||||
entryAttachmentState.attachment.binaryAttachment.length())
|
entryAttachmentState.attachment.binaryData.getSize())
|
||||||
holder.binaryFileCompression.apply {
|
holder.binaryFileCompression.apply {
|
||||||
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
|
if (entryAttachmentState.attachment.binaryData.isCompressed) {
|
||||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
@@ -105,6 +152,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
}
|
}
|
||||||
AttachmentState.NULL,
|
AttachmentState.NULL,
|
||||||
AttachmentState.ERROR,
|
AttachmentState.ERROR,
|
||||||
|
AttachmentState.CANCELED,
|
||||||
AttachmentState.COMPLETE -> {
|
AttachmentState.COMPLETE -> {
|
||||||
holder.binaryFileProgressContainer.visibility = View.GONE
|
holder.binaryFileProgressContainer.visibility = View.GONE
|
||||||
holder.binaryFileProgress.visibility = View.GONE
|
holder.binaryFileProgress.visibility = View.GONE
|
||||||
@@ -114,7 +162,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.itemView.setOnClickListener(null)
|
holder.binaryFileInfo.setOnClickListener(null)
|
||||||
}
|
}
|
||||||
StreamDirection.DOWNLOAD -> {
|
StreamDirection.DOWNLOAD -> {
|
||||||
holder.binaryFileProgressIcon.isActivated = false
|
holder.binaryFileProgressIcon.isActivated = false
|
||||||
@@ -122,12 +170,17 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
holder.binaryFileDeleteButton.visibility = View.GONE
|
holder.binaryFileDeleteButton.visibility = View.GONE
|
||||||
holder.binaryFileProgress.apply {
|
holder.binaryFileProgress.apply {
|
||||||
visibility = when (entryAttachmentState.downloadState) {
|
visibility = when (entryAttachmentState.downloadState) {
|
||||||
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
AttachmentState.NULL,
|
||||||
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
AttachmentState.COMPLETE,
|
||||||
|
AttachmentState.CANCELED,
|
||||||
|
AttachmentState.ERROR -> View.GONE
|
||||||
|
|
||||||
|
AttachmentState.START,
|
||||||
|
AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||||
}
|
}
|
||||||
progress = entryAttachmentState.downloadProgression
|
progress = entryAttachmentState.downloadProgression
|
||||||
}
|
}
|
||||||
holder.itemView.setOnClickListener {
|
holder.binaryFileInfo.setOnClickListener {
|
||||||
onItemClickListener?.invoke(entryAttachmentState)
|
onItemClickListener?.invoke(entryAttachmentState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,6 +189,8 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
|
|
||||||
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var binaryFileThumbnail: ImageView = itemView.findViewById(R.id.item_attachment_thumbnail)
|
||||||
|
var binaryFileInfo: View = itemView.findViewById(R.id.item_attachment_info)
|
||||||
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
|
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
|
||||||
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||||
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
|
||||||
|
class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tintIcon: Int)
|
||||||
|
: RecyclerView.Adapter<IconPickerAdapter<I>.CustomIconViewHolder>() {
|
||||||
|
|
||||||
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
|
private val iconList = ArrayList<I>()
|
||||||
|
|
||||||
|
var iconDrawableFactory: IconDrawableFactory? = null
|
||||||
|
var iconPickerListener: IconPickerListener<I>? = null
|
||||||
|
|
||||||
|
val lastPosition: Int
|
||||||
|
get() = iconList.lastIndex
|
||||||
|
|
||||||
|
fun addIcon(icon: I, notify: Boolean = true) {
|
||||||
|
if (!iconList.contains(icon)) {
|
||||||
|
iconList.add(icon)
|
||||||
|
if (notify) {
|
||||||
|
notifyItemInserted(iconList.indexOf(icon))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateIcon(icon: I) {
|
||||||
|
val index = iconList.indexOf(icon)
|
||||||
|
if (index != -1) {
|
||||||
|
iconList[index] = icon
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateIconSelectedState(icons: List<I>) {
|
||||||
|
icons.forEach { icon ->
|
||||||
|
val index = iconList.indexOf(icon)
|
||||||
|
if (index != -1
|
||||||
|
&& iconList[index].selected != icon.selected) {
|
||||||
|
iconList[index] = icon
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeIcon(icon: I) {
|
||||||
|
if (iconList.contains(icon)) {
|
||||||
|
val position = iconList.indexOf(icon)
|
||||||
|
iconList.remove(icon)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsAnySelectedIcon(): Boolean {
|
||||||
|
return iconList.firstOrNull { it.selected } != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deselectAllIcons() {
|
||||||
|
iconList.forEachIndexed { index, icon ->
|
||||||
|
if (icon.selected) {
|
||||||
|
icon.selected = false
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSelectedIcons(): List<I> {
|
||||||
|
return iconList.filter { it.selected }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
iconList.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setList(icons: List<I>) {
|
||||||
|
iconList.clear()
|
||||||
|
icons.forEach { iconImage ->
|
||||||
|
iconList.add(iconImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomIconViewHolder {
|
||||||
|
val view = inflater.inflate(R.layout.item_icon, parent, false)
|
||||||
|
return CustomIconViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) {
|
||||||
|
val icon = iconList[position]
|
||||||
|
iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon)
|
||||||
|
holder.iconContainerView.isSelected = icon.selected
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
iconPickerListener?.onIconClickListener(icon)
|
||||||
|
}
|
||||||
|
holder.itemView.setOnLongClickListener {
|
||||||
|
iconPickerListener?.onIconLongClickListener(icon)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return iconList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IconPickerListener<I: IconImageDraw> {
|
||||||
|
fun onIconClickListener(icon: I)
|
||||||
|
fun onIconLongClickListener(icon: I)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container)
|
||||||
|
var iconImageView: ImageView = itemView.findViewById(R.id.icon_image)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.IconCustomFragment
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.IconStandardFragment
|
||||||
|
|
||||||
|
class IconPickerPagerAdapter(fragment: Fragment, val size: Int)
|
||||||
|
: FragmentStateAdapter(fragment) {
|
||||||
|
|
||||||
|
private val iconStandardFragment = IconStandardFragment()
|
||||||
|
private val iconCustomFragment = IconCustomFragment()
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
return when (position) {
|
||||||
|
1 -> iconCustomFragment
|
||||||
|
else -> iconStandardFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SortedList
|
import androidx.recyclerview.widget.SortedList
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
@@ -39,7 +40,6 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
|
|||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.setTextSize
|
import com.kunzisoft.keepass.view.setTextSize
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
@@ -57,7 +57,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
private val mNodeSortedList: SortedList<Node>
|
private val mNodeSortedList: SortedList<Node>
|
||||||
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
private var mCalculateViewTypeTextSize = Array(2) { true} // number of view type
|
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
||||||
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||||
private var mPrefSizeMultiplier: Float = 0F
|
private var mPrefSizeMultiplier: Float = 0F
|
||||||
private var mSubtextDefaultDimension: Float = 0F
|
private var mSubtextDefaultDimension: Float = 0F
|
||||||
@@ -100,9 +100,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
this.mDatabase = Database.getInstance()
|
this.mDatabase = Database.getInstance()
|
||||||
|
|
||||||
// Color of content selection
|
// Color of content selection
|
||||||
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
||||||
this.mContentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
|
|
||||||
taContentSelectionColor.recycle()
|
|
||||||
// Retrieve the color to tint the icon
|
// Retrieve the color to tint the icon
|
||||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||||
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||||
@@ -305,7 +303,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
}
|
}
|
||||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||||
holder.icon.apply {
|
holder.icon.apply {
|
||||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
||||||
// Relative size of the icon
|
// Relative size of the icon
|
||||||
layoutParams?.apply {
|
layoutParams?.apply {
|
||||||
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import com.kunzisoft.keepass.database.element.Group
|
|||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
|
|
||||||
@@ -81,10 +80,9 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
val viewHolder = view.tag as ViewHolder
|
val viewHolder = view.tag as ViewHolder
|
||||||
|
|
||||||
// Assign image
|
// Assign image
|
||||||
viewHolder.imageViewIcon?.assignDatabaseIcon(
|
viewHolder.imageViewIcon?.let { iconView ->
|
||||||
database.drawFactory,
|
database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor)
|
||||||
currentEntry.icon,
|
}
|
||||||
iconColor)
|
|
||||||
|
|
||||||
// Assign title
|
// Assign title
|
||||||
viewHolder.textViewTitle?.apply {
|
viewHolder.textViewTitle?.apply {
|
||||||
@@ -110,10 +108,24 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
return database.createEntry()?.apply {
|
return database.createEntry()?.apply {
|
||||||
database.startManageEntry(this)
|
database.startManageEntry(this)
|
||||||
entryKDB?.let { entryKDB ->
|
entryKDB?.let { entryKDB ->
|
||||||
(cursor as EntryCursorKDB).populateEntry(entryKDB, database.iconFactory)
|
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
||||||
|
{ standardIconId ->
|
||||||
|
database.getStandardIcon(standardIconId)
|
||||||
|
},
|
||||||
|
{ customIconId ->
|
||||||
|
database.getCustomIcon(customIconId)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
entryKDBX?.let { entryKDBX ->
|
entryKDBX?.let { entryKDBX ->
|
||||||
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, database.iconFactory)
|
(cursor as EntryCursorKDBX).populateEntry(entryKDBX,
|
||||||
|
{ standardIconId ->
|
||||||
|
database.getStandardIcon(standardIconId)
|
||||||
|
},
|
||||||
|
{ customIconId ->
|
||||||
|
database.getCustomIcon(customIconId)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
database.stopManageEntry(this)
|
database.stopManageEntry(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import android.content.Intent
|
|||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
|
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.icons.createIconFromDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
@@ -86,6 +84,24 @@ object AutofillHelper {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun newRemoteViews(context: Context,
|
||||||
|
remoteViewsText: String,
|
||||||
|
remoteViewsIcon: IconImage? = null): RemoteViews {
|
||||||
|
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
||||||
|
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
||||||
|
if (remoteViewsIcon != null) {
|
||||||
|
try {
|
||||||
|
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
|
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
|
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return presentation
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildDataset(context: Context,
|
private fun buildDataset(context: Context,
|
||||||
entryInfo: EntryInfo,
|
entryInfo: EntryInfo,
|
||||||
struct: StructureParser.Result,
|
struct: StructureParser.Result,
|
||||||
@@ -116,6 +132,21 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to assign a drawable to a new icon from a database icon
|
||||||
|
*/
|
||||||
|
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
|
||||||
|
try {
|
||||||
|
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
|
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
|
return Icon.createWithBitmap(bitmap)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(context: Context,
|
||||||
@@ -267,26 +298,4 @@ object AutofillHelper {
|
|||||||
activity.finish()
|
activity.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newRemoteViews(context: Context,
|
|
||||||
remoteViewsText: String,
|
|
||||||
remoteViewsIcon: IconImage? = null): RemoteViews {
|
|
||||||
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
|
||||||
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
|
||||||
if (remoteViewsIcon != null) {
|
|
||||||
presentation.assignDatabaseIcon(context,
|
|
||||||
R.id.autofill_entry_icon,
|
|
||||||
Database.getInstance().drawFactory,
|
|
||||||
remoteViewsIcon,
|
|
||||||
ContextCompat.getColor(context, R.color.green))
|
|
||||||
}
|
|
||||||
return presentation
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
|
|
||||||
return createIconFromDatabaseIcon(context,
|
|
||||||
Database.getInstance().drawFactory,
|
|
||||||
entryInfo.icon,
|
|
||||||
ContextCompat.getColor(context, R.color.green))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.autofill
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BlendMode
|
import android.graphics.BlendMode
|
||||||
@@ -130,6 +131,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
|
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
|
||||||
|
|||||||
@@ -24,39 +24,26 @@ import android.net.Uri
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
open class AssignPasswordInDatabaseRunnable (
|
open class AssignPasswordInDatabaseRunnable (
|
||||||
context: Context,
|
context: Context,
|
||||||
database: Database,
|
database: Database,
|
||||||
protected val mDatabaseUri: Uri,
|
protected val mDatabaseUri: Uri,
|
||||||
withMasterPassword: Boolean,
|
protected val mMainCredential: MainCredential)
|
||||||
masterPassword: String?,
|
|
||||||
withKeyFile: Boolean,
|
|
||||||
keyFile: Uri?)
|
|
||||||
: SaveDatabaseRunnable(context, database, true) {
|
: SaveDatabaseRunnable(context, database, true) {
|
||||||
|
|
||||||
private var mMasterPassword: String? = null
|
|
||||||
protected var mKeyFileUri: Uri? = null
|
|
||||||
|
|
||||||
private var mBackupKey: ByteArray? = null
|
private var mBackupKey: ByteArray? = null
|
||||||
|
|
||||||
init {
|
|
||||||
if (withMasterPassword)
|
|
||||||
this.mMasterPassword = masterPassword
|
|
||||||
if (withKeyFile)
|
|
||||||
this.mKeyFileUri = keyFile
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
// Set key
|
// Set key
|
||||||
try {
|
try {
|
||||||
// TODO move master key methods
|
|
||||||
mBackupKey = ByteArray(database.masterKey.size)
|
mBackupKey = ByteArray(database.masterKey.size)
|
||||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||||
|
|
||||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri)
|
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
||||||
database.retrieveMasterKey(mMasterPassword, uriInputStream)
|
database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
erase(mBackupKey)
|
erase(mBackupKey)
|
||||||
setError(e)
|
setError(e)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import android.net.Uri
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
@@ -32,12 +33,9 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
databaseUri: Uri,
|
databaseUri: Uri,
|
||||||
private val databaseName: String,
|
private val databaseName: String,
|
||||||
private val rootName: String,
|
private val rootName: String,
|
||||||
withMasterPassword: Boolean,
|
mainCredential: MainCredential,
|
||||||
masterPassword: String?,
|
|
||||||
withKeyFile: Boolean,
|
|
||||||
keyFile: Uri?,
|
|
||||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
|
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
try {
|
try {
|
||||||
@@ -61,7 +59,7 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
.addOrUpdateDatabaseUri(mDatabaseUri,
|
||||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFileUri else null)
|
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
@@ -35,8 +35,7 @@ import com.kunzisoft.keepass.utils.UriUtil
|
|||||||
class LoadDatabaseRunnable(private val context: Context,
|
class LoadDatabaseRunnable(private val context: Context,
|
||||||
private val mDatabase: Database,
|
private val mDatabase: Database,
|
||||||
private val mUri: Uri,
|
private val mUri: Uri,
|
||||||
private val mPass: String?,
|
private val mMainCredential: MainCredential,
|
||||||
private val mKey: Uri?,
|
|
||||||
private val mReadonly: Boolean,
|
private val mReadonly: Boolean,
|
||||||
private val mCipherEntity: CipherDatabaseEntity?,
|
private val mCipherEntity: CipherDatabaseEntity?,
|
||||||
private val mFixDuplicateUUID: Boolean,
|
private val mFixDuplicateUUID: Boolean,
|
||||||
@@ -51,10 +50,12 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
try {
|
try {
|
||||||
mDatabase.loadData(mUri, mPass, mKey,
|
mDatabase.loadData(mUri,
|
||||||
|
mMainCredential,
|
||||||
mReadonly,
|
mReadonly,
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
UriUtil.getBinaryDir(context),
|
UriUtil.getBinaryDir(context),
|
||||||
|
Database.LoadedKey.generateNewCipherKey(),
|
||||||
mFixDuplicateUUID,
|
mFixDuplicateUUID,
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
}
|
||||||
@@ -67,7 +68,7 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||||
FileDatabaseHistoryAction.getInstance(context)
|
FileDatabaseHistoryAction.getInstance(context)
|
||||||
.addOrUpdateDatabaseUri(mUri,
|
.addOrUpdateDatabaseUri(mUri,
|
||||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null)
|
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the biometric
|
// Register the biometric
|
||||||
|
|||||||
@@ -37,36 +37,37 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||||
@@ -264,30 +265,22 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseCreate(databaseUri: Uri,
|
fun startDatabaseCreate(databaseUri: Uri,
|
||||||
masterPasswordChecked: Boolean,
|
mainCredential: MainCredential) {
|
||||||
masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean,
|
|
||||||
keyFile: Uri?) {
|
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
|
||||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_CREATE_TASK)
|
, ACTION_DATABASE_CREATE_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseLoad(databaseUri: Uri,
|
fun startDatabaseLoad(databaseUri: Uri,
|
||||||
masterPassword: String?,
|
mainCredential: MainCredential,
|
||||||
keyFile: Uri?,
|
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherEntity: CipherDatabaseEntity?,
|
cipherEntity: CipherDatabaseEntity?,
|
||||||
fixDuplicateUuid: Boolean) {
|
fixDuplicateUuid: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
|
||||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||||
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
@@ -303,17 +296,11 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||||
masterPasswordChecked: Boolean,
|
mainCredential: MainCredential) {
|
||||||
masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean,
|
|
||||||
keyFile: Uri?) {
|
|
||||||
|
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
|
||||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.database.action
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -34,18 +33,22 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
: ActionRunnable() {
|
: ActionRunnable() {
|
||||||
|
|
||||||
|
private var tempCipherKey: Database.LoadedKey? = null
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
|
tempCipherKey = mDatabase.loadedCipherKey
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.clear(UriUtil.getBinaryDir(context))
|
mDatabase.clear(UriUtil.getBinaryDir(context))
|
||||||
|
mDatabase.wasReloaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
try {
|
try {
|
||||||
mDatabase.reloadData(context.contentResolver,
|
mDatabase.reloadData(context.contentResolver,
|
||||||
UriUtil.getBinaryDir(context),
|
UriUtil.getBinaryDir(context),
|
||||||
|
tempCipherKey ?: Database.LoadedKey.generateNewCipherKey(),
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
} catch (e: LoadDatabaseException) {
|
||||||
catch (e: LoadDatabaseException) {
|
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +56,7 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
} else {
|
} else {
|
||||||
|
tempCipherKey = null
|
||||||
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,16 +52,9 @@ class CopyNodesRunnable constructor(
|
|||||||
if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) {
|
if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) {
|
||||||
// Update entry with new values
|
// Update entry with new values
|
||||||
mNewParent.touch(modified = false, touchParents = true)
|
mNewParent.touch(modified = false, touchParents = true)
|
||||||
|
|
||||||
val entryCopied = database.copyEntryTo(currentNode as Entry, mNewParent)
|
val entryCopied = database.copyEntryTo(currentNode as Entry, mNewParent)
|
||||||
if (entryCopied != null) {
|
entryCopied.touch(modified = true, touchParents = true)
|
||||||
entryCopied.touch(modified = true, touchParents = true)
|
mEntriesCopied.add(entryCopied)
|
||||||
mEntriesCopied.add(entryCopied)
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Unable to create a copy of the entry")
|
|
||||||
setError(CopyEntryDatabaseException())
|
|
||||||
break@foreachNode
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Only finish thread
|
// Only finish thread
|
||||||
setError(CopyEntryDatabaseException())
|
setError(CopyEntryDatabaseException())
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class DeleteNodesRunnable(context: Context,
|
|||||||
database.deleteEntry(currentNode)
|
database.deleteEntry(currentNode)
|
||||||
}
|
}
|
||||||
// Remove the oldest attachments
|
// Remove the oldest attachments
|
||||||
currentNode.getAttachments(database.binaryPool).forEach {
|
currentNode.getAttachments(database.attachmentPool).forEach {
|
||||||
database.removeAttachmentIfNotUsed(it)
|
database.removeAttachmentIfNotUsed(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ class UpdateEntryRunnable constructor(
|
|||||||
mNewEntry.addParentFrom(mOldEntry)
|
mNewEntry.addParentFrom(mOldEntry)
|
||||||
|
|
||||||
// Build oldest attachments
|
// Build oldest attachments
|
||||||
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true)
|
val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
|
||||||
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true)
|
val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true)
|
||||||
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
||||||
// Not use equals because only check name
|
// Not use equals because only check name
|
||||||
newEntryAttachments.forEach { newAttachment ->
|
newEntryAttachments.forEach { newAttachment ->
|
||||||
oldEntryAttachments.forEach { oldAttachment ->
|
oldEntryAttachments.forEach { oldAttachment ->
|
||||||
if (oldAttachment.name == newAttachment.name
|
if (oldAttachment.name == newAttachment.name
|
||||||
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment)
|
&& oldAttachment.binaryData == newAttachment.binaryData)
|
||||||
attachmentsToRemove.remove(oldAttachment)
|
attachmentsToRemove.remove(oldAttachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ class UpdateEntryRunnable constructor(
|
|||||||
|
|
||||||
// Create an entry history (an entry history don't have history)
|
// Create an entry history (an entry history don't have history)
|
||||||
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
||||||
database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
|
database.removeOldestEntryHistory(mOldEntry, database.attachmentPool)
|
||||||
|
|
||||||
// Only change data in index
|
// Only change data in index
|
||||||
database.updateEntry(mOldEntry)
|
database.updateEntry(mOldEntry)
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import android.database.MatrixCursor
|
|||||||
import android.provider.BaseColumns
|
import android.provider.BaseColumns
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -49,12 +51,16 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
|
|||||||
|
|
||||||
abstract fun getPwNodeId(): NodeId<EntryId>
|
abstract fun getPwNodeId(): NodeId<EntryId>
|
||||||
|
|
||||||
open fun populateEntry(pwEntry: PwEntryV, iconFactory: IconImageFactory) {
|
open fun populateEntry(pwEntry: PwEntryV,
|
||||||
|
retrieveStandardIcon: (Int) -> IconImageStandard,
|
||||||
|
retrieveCustomIcon: (UUID) -> IconImageCustom) {
|
||||||
pwEntry.nodeId = getPwNodeId()
|
pwEntry.nodeId = getPwNodeId()
|
||||||
pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE))
|
pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE))
|
||||||
|
|
||||||
val iconStandard = iconFactory.getIcon(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD)))
|
val iconStandard = retrieveStandardIcon.invoke(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD)))
|
||||||
pwEntry.icon = iconStandard
|
val iconCustom = retrieveCustomIcon.invoke(UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
|
||||||
|
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
|
||||||
|
pwEntry.icon = IconImage(iconStandard, iconCustom)
|
||||||
|
|
||||||
pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME))
|
pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME))
|
||||||
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))
|
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.cursor
|
package com.kunzisoft.keepass.database.cursor
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
|
|
||||||
class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
|
class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
|
||||||
@@ -30,9 +29,9 @@ class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
|
|||||||
entry.id.mostSignificantBits,
|
entry.id.mostSignificantBits,
|
||||||
entry.id.leastSignificantBits,
|
entry.id.leastSignificantBits,
|
||||||
entry.title,
|
entry.title,
|
||||||
entry.icon.iconId,
|
entry.icon.standard.id,
|
||||||
DatabaseVersioned.UUID_ZERO.mostSignificantBits,
|
entry.icon.custom.uuid.mostSignificantBits,
|
||||||
DatabaseVersioned.UUID_ZERO.leastSignificantBits,
|
entry.icon.custom.uuid.leastSignificantBits,
|
||||||
entry.username,
|
entry.username,
|
||||||
entry.password,
|
entry.password,
|
||||||
entry.url,
|
entry.url,
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
package com.kunzisoft.keepass.database.cursor
|
package com.kunzisoft.keepass.database.cursor
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
|||||||
entry.id.mostSignificantBits,
|
entry.id.mostSignificantBits,
|
||||||
entry.id.leastSignificantBits,
|
entry.id.leastSignificantBits,
|
||||||
entry.title,
|
entry.title,
|
||||||
entry.icon.iconId,
|
entry.icon.standard.id,
|
||||||
entry.iconCustom.uuid.mostSignificantBits,
|
entry.icon.custom.uuid.mostSignificantBits,
|
||||||
entry.iconCustom.uuid.leastSignificantBits,
|
entry.icon.custom.uuid.leastSignificantBits,
|
||||||
entry.username,
|
entry.username,
|
||||||
entry.password,
|
entry.password,
|
||||||
entry.url,
|
entry.url,
|
||||||
@@ -52,14 +52,10 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
|||||||
entryId++
|
entryId++
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun populateEntry(pwEntry: EntryKDBX, iconFactory: IconImageFactory) {
|
override fun populateEntry(pwEntry: EntryKDBX,
|
||||||
super.populateEntry(pwEntry, iconFactory)
|
retrieveStandardIcon: (Int) -> IconImageStandard,
|
||||||
|
retrieveCustomIcon: (UUID) -> IconImageCustom) {
|
||||||
// Retrieve custom icon
|
super.populateEntry(pwEntry, retrieveStandardIcon, retrieveCustomIcon)
|
||||||
val iconCustom = iconFactory.getIcon(
|
|
||||||
UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
|
|
||||||
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
|
|
||||||
pwEntry.iconCustom = iconCustom
|
|
||||||
|
|
||||||
// Retrieve extra fields
|
// Retrieve extra fields
|
||||||
if (extraFieldCursor.moveToFirst()) {
|
if (extraFieldCursor.moveToFirst()) {
|
||||||
|
|||||||
@@ -21,19 +21,21 @@ package com.kunzisoft.keepass.database.element
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
import com.kunzisoft.keepass.database.element.database.BinaryByte
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryData
|
||||||
|
|
||||||
|
|
||||||
data class Attachment(var name: String,
|
data class Attachment(var name: String,
|
||||||
var binaryAttachment: BinaryAttachment) : Parcelable {
|
var binaryData: BinaryData) : Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readString() ?: "",
|
parcel.readString() ?: "",
|
||||||
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment()
|
parcel.readParcelable(BinaryData::class.java.classLoader) ?: BinaryByte()
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeString(name)
|
parcel.writeString(name)
|
||||||
parcel.writeParcelable(binaryAttachment, flags)
|
parcel.writeParcelable(binaryData, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
@@ -41,7 +43,7 @@ data class Attachment(var name: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "$name at $binaryAttachment"
|
return "$name at $binaryData"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ import android.util.Log
|
|||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.element.database.*
|
import com.kunzisoft.keepass.database.element.database.*
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconsManager
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
@@ -41,12 +43,16 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
|||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.stream.readBytes4ToUInt
|
import com.kunzisoft.keepass.stream.readBytes4ToUInt
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
import java.security.Key
|
||||||
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
@@ -63,7 +69,10 @@ class Database {
|
|||||||
|
|
||||||
var isReadOnly = false
|
var isReadOnly = false
|
||||||
|
|
||||||
val drawFactory = IconDrawableFactory()
|
val iconDrawableFactory = IconDrawableFactory(
|
||||||
|
{ loadedCipherKey },
|
||||||
|
{ iconId -> iconsManager.getBinaryForCustomIcon(iconId) }
|
||||||
|
)
|
||||||
|
|
||||||
var loaded = false
|
var loaded = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -71,13 +80,64 @@ class Database {
|
|||||||
loadTimestamp = if (field) System.currentTimeMillis() else null
|
loadTimestamp = if (field) System.currentTimeMillis() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To reload the main activity
|
||||||
|
*/
|
||||||
|
var wasReloaded = false
|
||||||
|
|
||||||
var loadTimestamp: Long? = null
|
var loadTimestamp: Long? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val iconFactory: IconImageFactory
|
/**
|
||||||
get() {
|
* Cipher key regenerated when the database is loaded and closed
|
||||||
return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
|
* Can be used to temporarily store database elements
|
||||||
|
*/
|
||||||
|
var loadedCipherKey: LoadedKey?
|
||||||
|
private set(value) {
|
||||||
|
mDatabaseKDB?.loadedCipherKey = value
|
||||||
|
mDatabaseKDBX?.loadedCipherKey = value
|
||||||
}
|
}
|
||||||
|
get() {
|
||||||
|
return mDatabaseKDB?.loadedCipherKey ?: mDatabaseKDBX?.loadedCipherKey
|
||||||
|
}
|
||||||
|
|
||||||
|
private val iconsManager: IconsManager
|
||||||
|
get() {
|
||||||
|
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
|
||||||
|
return iconsManager.doForEachStandardIcon(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStandardIcon(iconId: Int): IconImageStandard {
|
||||||
|
return iconsManager.getIcon(iconId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowCustomIcons: Boolean
|
||||||
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
|
fun doForEachCustomIcons(action: (IconImageCustom, BinaryData) -> Unit) {
|
||||||
|
return iconsManager.doForEachCustomIcon(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCustomIcon(iconId: UUID): IconImageCustom {
|
||||||
|
return iconsManager.getIcon(iconId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildNewCustomIcon(cacheDirectory: File,
|
||||||
|
result: (IconImageCustom?, BinaryData?) -> Unit) {
|
||||||
|
mDatabaseKDBX?.buildNewCustomIcon(cacheDirectory, null, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
||||||
|
return mDatabaseKDBX?.isCustomIconBinaryDuplicate(binaryData) ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCustomIcon(customIcon: IconImageCustom) {
|
||||||
|
iconDrawableFactory.clearFromCache(customIcon)
|
||||||
|
iconsManager.removeCustomIcon(customIcon.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
val allowName: Boolean
|
val allowName: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
@@ -320,12 +380,26 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
||||||
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
|
val newDatabase = DatabaseKDBX(databaseName, rootName)
|
||||||
|
newDatabase.loadedCipherKey = LoadedKey.generateNewCipherKey()
|
||||||
|
setDatabaseKDBX(newDatabase)
|
||||||
this.fileUri = databaseUri
|
this.fileUri = databaseUri
|
||||||
// Set Database state
|
// Set Database state
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LoadedKey(val key: Key, val iv: ByteArray): Serializable {
|
||||||
|
companion object {
|
||||||
|
const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding"
|
||||||
|
|
||||||
|
fun generateNewCipherKey(): LoadedKey {
|
||||||
|
val iv = ByteArray(8)
|
||||||
|
SecureRandom().nextBytes(iv)
|
||||||
|
return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
|
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
|
||||||
openDatabaseKDB: (InputStream) -> DatabaseKDB,
|
openDatabaseKDB: (InputStream) -> DatabaseKDB,
|
||||||
@@ -366,16 +440,20 @@ class Database {
|
|||||||
loaded = true
|
loaded = true
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
throw e
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw LoadDatabaseException(e)
|
||||||
} finally {
|
} finally {
|
||||||
databaseInputStream?.close()
|
databaseInputStream?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
fun loadData(uri: Uri, password: String?, keyfile: Uri?,
|
fun loadData(uri: Uri,
|
||||||
|
mainCredential: MainCredential,
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
cacheDirectory: File,
|
cacheDirectory: File,
|
||||||
|
tempCipherKey: LoadedKey,
|
||||||
fixDuplicateUUID: Boolean,
|
fixDuplicateUUID: Boolean,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
@@ -389,8 +467,8 @@ class Database {
|
|||||||
var keyFileInputStream: InputStream? = null
|
var keyFileInputStream: InputStream? = null
|
||||||
try {
|
try {
|
||||||
// Get keyFile inputStream
|
// Get keyFile inputStream
|
||||||
keyfile?.let {
|
mainCredential.keyFileUri?.let { keyFile ->
|
||||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
|
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read database stream for the first time
|
// Read database stream for the first time
|
||||||
@@ -398,16 +476,18 @@ class Database {
|
|||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDB(cacheDirectory)
|
DatabaseInputKDB(cacheDirectory)
|
||||||
.openDatabase(databaseInputStream,
|
.openDatabase(databaseInputStream,
|
||||||
password,
|
mainCredential.masterPassword,
|
||||||
keyFileInputStream,
|
keyFileInputStream,
|
||||||
|
tempCipherKey,
|
||||||
progressTaskUpdater,
|
progressTaskUpdater,
|
||||||
fixDuplicateUUID)
|
fixDuplicateUUID)
|
||||||
},
|
},
|
||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDBX(cacheDirectory)
|
DatabaseInputKDBX(cacheDirectory)
|
||||||
.openDatabase(databaseInputStream,
|
.openDatabase(databaseInputStream,
|
||||||
password,
|
mainCredential.masterPassword,
|
||||||
keyFileInputStream,
|
keyFileInputStream,
|
||||||
|
tempCipherKey,
|
||||||
progressTaskUpdater,
|
progressTaskUpdater,
|
||||||
fixDuplicateUUID)
|
fixDuplicateUUID)
|
||||||
}
|
}
|
||||||
@@ -427,27 +507,39 @@ class Database {
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
fun reloadData(contentResolver: ContentResolver,
|
fun reloadData(contentResolver: ContentResolver,
|
||||||
cacheDirectory: File,
|
cacheDirectory: File,
|
||||||
|
tempCipherKey: LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
// Retrieve the stream from the old database URI
|
// Retrieve the stream from the old database URI
|
||||||
fileUri?.let { oldDatabaseUri ->
|
try {
|
||||||
readDatabaseStream(contentResolver, oldDatabaseUri,
|
fileUri?.let { oldDatabaseUri ->
|
||||||
{ databaseInputStream ->
|
readDatabaseStream(contentResolver, oldDatabaseUri,
|
||||||
DatabaseInputKDB(cacheDirectory)
|
{ databaseInputStream ->
|
||||||
.openDatabase(databaseInputStream,
|
DatabaseInputKDB(cacheDirectory)
|
||||||
masterKey,
|
.openDatabase(databaseInputStream,
|
||||||
progressTaskUpdater)
|
masterKey,
|
||||||
},
|
tempCipherKey,
|
||||||
{ databaseInputStream ->
|
progressTaskUpdater)
|
||||||
DatabaseInputKDBX(cacheDirectory)
|
},
|
||||||
.openDatabase(databaseInputStream,
|
{ databaseInputStream ->
|
||||||
masterKey,
|
DatabaseInputKDBX(cacheDirectory)
|
||||||
progressTaskUpdater)
|
.openDatabase(databaseInputStream,
|
||||||
}
|
masterKey,
|
||||||
)
|
tempCipherKey,
|
||||||
} ?: run {
|
progressTaskUpdater)
|
||||||
Log.e(TAG, "Database URI is null, database cannot be reloaded")
|
}
|
||||||
throw IODatabaseException()
|
)
|
||||||
|
} ?: run {
|
||||||
|
Log.e(TAG, "Database URI is null, database cannot be reloaded")
|
||||||
|
throw IODatabaseException()
|
||||||
|
}
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
Log.e(TAG, "Unable to load keyfile", e)
|
||||||
|
throw FileNotFoundDatabaseException()
|
||||||
|
} catch (e: LoadDatabaseException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw LoadDatabaseException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,9 +574,10 @@ class Database {
|
|||||||
}, omitBackup, max)
|
}, omitBackup, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
val binaryPool: BinaryPool
|
val attachmentPool: AttachmentPool
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDBX?.binaryPool ?: BinaryPool()
|
// Binary pool is functionally only in KDBX
|
||||||
|
return mDatabaseKDBX?.binaryPool ?: AttachmentPool()
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowMultipleAttachments: Boolean
|
val allowMultipleAttachments: Boolean
|
||||||
@@ -496,17 +589,17 @@ class Database {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewBinary(cacheDirectory: File,
|
fun buildNewBinaryAttachment(cacheDirectory: File,
|
||||||
compressed: Boolean = false,
|
compressed: Boolean = false,
|
||||||
protected: Boolean = false): BinaryAttachment? {
|
protected: Boolean = false): BinaryData? {
|
||||||
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
|
return mDatabaseKDB?.buildNewAttachment(cacheDirectory)
|
||||||
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, compressed, protected)
|
?: mDatabaseKDBX?.buildNewAttachment(cacheDirectory, compressed, protected)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||||
// No need in KDB database because unique attachment by entry
|
// No need in KDB database because unique attachment by entry
|
||||||
// Don't clear to fix upload multiple times
|
// Don't clear to fix upload multiple times
|
||||||
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false)
|
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryData, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeUnlinkedAttachments() {
|
fun removeUnlinkedAttachments() {
|
||||||
@@ -575,7 +668,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clear(filesDirectory: File? = null) {
|
fun clear(filesDirectory: File? = null) {
|
||||||
drawFactory.clearCache()
|
iconsManager.clearCache()
|
||||||
|
iconDrawableFactory.clearCache()
|
||||||
// Delete the cache of the database if present
|
// Delete the cache of the database if present
|
||||||
mDatabaseKDB?.clearCache()
|
mDatabaseKDB?.clearCache()
|
||||||
mDatabaseKDBX?.clearCache()
|
mDatabaseKDBX?.clearCache()
|
||||||
@@ -608,7 +702,9 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
fun validatePasswordEncoding(mainCredential: MainCredential): Boolean {
|
||||||
|
val password = mainCredential.masterPassword
|
||||||
|
val containsKeyFile = mainCredential.keyFileUri != null
|
||||||
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
|
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
|
||||||
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
|
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
|
||||||
?: false
|
?: false
|
||||||
@@ -739,7 +835,7 @@ class Database {
|
|||||||
* @param entryToCopy
|
* @param entryToCopy
|
||||||
* @param newParent
|
* @param newParent
|
||||||
*/
|
*/
|
||||||
fun copyEntryTo(entryToCopy: Entry, newParent: Group): Entry? {
|
fun copyEntryTo(entryToCopy: Entry, newParent: Group): Entry {
|
||||||
val entryCopied = Entry(entryToCopy, false)
|
val entryCopied = Entry(entryToCopy, false)
|
||||||
entryCopied.nodeId = mDatabaseKDB?.newEntryId() ?: mDatabaseKDBX?.newEntryId() ?: NodeIdUUID()
|
entryCopied.nodeId = mDatabaseKDB?.newEntryId() ?: mDatabaseKDBX?.newEntryId() ?: NodeIdUUID()
|
||||||
entryCopied.parent = newParent
|
entryCopied.parent = newParent
|
||||||
@@ -896,7 +992,7 @@ class Database {
|
|||||||
rootGroup?.doForEachChildAndForIt(
|
rootGroup?.doForEachChildAndForIt(
|
||||||
object : NodeHandler<Entry>() {
|
object : NodeHandler<Entry>() {
|
||||||
override fun operate(node: Entry): Boolean {
|
override fun operate(node: Entry): Boolean {
|
||||||
removeOldestEntryHistory(node, binaryPool)
|
removeOldestEntryHistory(node, attachmentPool)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -911,7 +1007,7 @@ class Database {
|
|||||||
/**
|
/**
|
||||||
* Remove oldest history if more than max items or max memory
|
* Remove oldest history if more than max items or max memory
|
||||||
*/
|
*/
|
||||||
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
|
fun removeOldestEntryHistory(entry: Entry, attachmentPool: AttachmentPool) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
val maxItems = historyMaxItems
|
val maxItems = historyMaxItems
|
||||||
if (maxItems >= 0) {
|
if (maxItems >= 0) {
|
||||||
@@ -925,7 +1021,7 @@ class Database {
|
|||||||
while (true) {
|
while (true) {
|
||||||
var historySize: Long = 0
|
var historySize: Long = 0
|
||||||
for (entryHistory in entry.getHistory()) {
|
for (entryHistory in entry.getHistory()) {
|
||||||
historySize += entryHistory.getSize(binaryPool)
|
historySize += entryHistory.getSize(attachmentPool)
|
||||||
}
|
}
|
||||||
if (historySize > maxSize) {
|
if (historySize > maxSize) {
|
||||||
removeOldestEntryHistory(entry)
|
removeOldestEntryHistory(entry)
|
||||||
@@ -939,7 +1035,7 @@ class Database {
|
|||||||
|
|
||||||
private fun removeOldestEntryHistory(entry: Entry) {
|
private fun removeOldestEntryHistory(entry: Entry) {
|
||||||
entry.removeOldestEntryFromHistory()?.let {
|
entry.removeOldestEntryFromHistory()?.let {
|
||||||
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
it.getAttachments(attachmentPool, false).forEach { attachmentToRemove ->
|
||||||
removeAttachmentIfNotUsed(attachmentToRemove)
|
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -947,7 +1043,7 @@ class Database {
|
|||||||
|
|
||||||
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
|
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
|
||||||
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
|
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
|
||||||
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
it.getAttachments(attachmentPool, false).forEach { attachmentToRemove ->
|
||||||
removeAttachmentIfNotUsed(attachmentToRemove)
|
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,12 @@ package com.kunzisoft.keepass.database.element
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryPool
|
import com.kunzisoft.keepass.database.element.database.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
|
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
@@ -109,7 +107,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
|
|
||||||
override var icon: IconImage
|
override var icon: IconImage
|
||||||
get() {
|
get() {
|
||||||
return entryKDB?.icon ?: entryKDBX?.icon ?: IconImageStandard()
|
return entryKDB?.icon ?: entryKDBX?.icon ?: IconImage()
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
entryKDB?.icon = value
|
entryKDB?.icon = value
|
||||||
@@ -257,31 +255,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
------------
|
|
||||||
KDB Methods
|
|
||||||
------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If it's a node with only meta information like Meta-info SYSTEM Database Color
|
|
||||||
* @return false by default, true if it's a meta stream
|
|
||||||
*/
|
|
||||||
val isMetaStream: Boolean
|
|
||||||
get() = entryKDB?.isMetaStream ?: false
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------
|
------------
|
||||||
KDBX Methods
|
KDBX Methods
|
||||||
------------
|
------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var iconCustom: IconImageCustom
|
|
||||||
get() = entryKDBX?.iconCustom ?: IconImageCustom.UNKNOWN_ICON
|
|
||||||
set(value) {
|
|
||||||
entryKDBX?.iconCustom = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
|
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
|
||||||
* @return Map of label/value
|
* @return Map of label/value
|
||||||
@@ -330,12 +309,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryKDBX?.stopToManageFieldReferences()
|
entryKDBX?.stopToManageFieldReferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
|
fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
|
||||||
val attachments = ArrayList<Attachment>()
|
val attachments = ArrayList<Attachment>()
|
||||||
entryKDB?.getAttachment()?.let {
|
entryKDB?.getAttachment()?.let {
|
||||||
attachments.add(it)
|
attachments.add(it)
|
||||||
}
|
}
|
||||||
entryKDBX?.getAttachments(binaryPool, inHistory)?.let {
|
entryKDBX?.getAttachments(attachmentPool, inHistory)?.let {
|
||||||
attachments.addAll(it)
|
attachments.addAll(it)
|
||||||
}
|
}
|
||||||
return attachments
|
return attachments
|
||||||
@@ -346,12 +325,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
|| entryKDBX?.containsAttachment() == true
|
|| entryKDBX?.containsAttachment() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
|
|
||||||
attachments.forEach {
|
|
||||||
putAttachment(it, binaryPool)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeAttachment(attachment: Attachment) {
|
private fun removeAttachment(attachment: Attachment) {
|
||||||
entryKDB?.removeAttachment(attachment)
|
entryKDB?.removeAttachment(attachment)
|
||||||
entryKDBX?.removeAttachment(attachment)
|
entryKDBX?.removeAttachment(attachment)
|
||||||
@@ -362,9 +335,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryKDBX?.removeAttachments()
|
entryKDBX?.removeAttachments()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
private fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
|
||||||
entryKDB?.putAttachment(attachment)
|
entryKDB?.putAttachment(attachment)
|
||||||
entryKDBX?.putAttachment(attachment, binaryPool)
|
entryKDBX?.putAttachment(attachment, attachmentPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHistory(): ArrayList<Entry> {
|
fun getHistory(): ArrayList<Entry> {
|
||||||
@@ -396,8 +369,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSize(binaryPool: BinaryPool): Long {
|
fun getSize(attachmentPool: AttachmentPool): Long {
|
||||||
return entryKDBX?.getSize(binaryPool) ?: 0L
|
return entryKDBX?.getSize(attachmentPool) ?: 0L
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCustomData(): Boolean {
|
fun containsCustomData(): Boolean {
|
||||||
@@ -427,7 +400,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryInfo.username = username
|
entryInfo.username = username
|
||||||
entryInfo.password = password
|
entryInfo.password = password
|
||||||
entryInfo.creationTime = creationTime
|
entryInfo.creationTime = creationTime
|
||||||
entryInfo.modificationTime = lastModificationTime
|
entryInfo.lastModificationTime = lastModificationTime
|
||||||
entryInfo.expires = expires
|
entryInfo.expires = expires
|
||||||
entryInfo.expiryTime = expiryTime
|
entryInfo.expiryTime = expiryTime
|
||||||
entryInfo.url = url
|
entryInfo.url = url
|
||||||
@@ -439,7 +412,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
// Replace parameter fields by generated OTP fields
|
// Replace parameter fields by generated OTP fields
|
||||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||||
}
|
}
|
||||||
database?.binaryPool?.let { binaryPool ->
|
database?.attachmentPool?.let { binaryPool ->
|
||||||
entryInfo.attachments = getAttachments(binaryPool)
|
entryInfo.attachments = getAttachments(binaryPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,8 +439,10 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
url = newEntryInfo.url
|
url = newEntryInfo.url
|
||||||
notes = newEntryInfo.notes
|
notes = newEntryInfo.notes
|
||||||
addExtraFields(newEntryInfo.customFields)
|
addExtraFields(newEntryInfo.customFields)
|
||||||
database?.binaryPool?.let { binaryPool ->
|
database?.attachmentPool?.let { binaryPool ->
|
||||||
addAttachments(binaryPool, newEntryInfo.attachments)
|
newEntryInfo.attachments.forEach { attachment ->
|
||||||
|
putAttachment(attachment, binaryPool)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
database?.stopManageEntry(this)
|
database?.stopManageEntry(this)
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ import com.kunzisoft.keepass.database.element.group.GroupKDB
|
|||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.database.element.node.*
|
import com.kunzisoft.keepass.database.element.node.*
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
@@ -40,6 +40,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
var groupKDBX: GroupKDBX? = null
|
var groupKDBX: GroupKDBX? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
// Virtual group is used to defined a detached database group
|
||||||
|
var isVirtual = false
|
||||||
|
|
||||||
fun updateWith(group: Group) {
|
fun updateWith(group: Group) {
|
||||||
group.groupKDB?.let {
|
group.groupKDB?.let {
|
||||||
this.groupKDB?.updateWith(it)
|
this.groupKDB?.updateWith(it)
|
||||||
@@ -77,6 +80,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
groupKDB = parcel.readParcelable(GroupKDB::class.java.classLoader)
|
groupKDB = parcel.readParcelable(GroupKDB::class.java.classLoader)
|
||||||
groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
||||||
|
isVirtual = parcel.readByte().toInt() != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ChildFilter {
|
enum class ChildFilter {
|
||||||
@@ -110,6 +114,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
dest.writeParcelable(groupKDB, flags)
|
dest.writeParcelable(groupKDB, flags)
|
||||||
dest.writeParcelable(groupKDBX, flags)
|
dest.writeParcelable(groupKDBX, flags)
|
||||||
|
dest.writeByte((if (isVirtual) 1 else 0).toByte())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val nodeId: NodeId<*>?
|
override val nodeId: NodeId<*>?
|
||||||
@@ -123,7 +128,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override var icon: IconImage
|
override var icon: IconImage
|
||||||
get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImageStandard()
|
get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImage()
|
||||||
set(value) {
|
set(value) {
|
||||||
groupKDB?.icon = value
|
groupKDB?.icon = value
|
||||||
groupKDBX?.icon = value
|
groupKDBX?.icon = value
|
||||||
@@ -232,6 +237,14 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
override val isCurrentlyExpires: Boolean
|
override val isCurrentlyExpires: Boolean
|
||||||
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
|
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
|
||||||
|
|
||||||
|
var notes: String?
|
||||||
|
get() = groupKDBX?.notes
|
||||||
|
set(value) {
|
||||||
|
value?.let {
|
||||||
|
groupKDBX?.notes = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getChildGroups(): List<Group> {
|
override fun getChildGroups(): List<Group> {
|
||||||
return groupKDB?.getChildGroups()?.map {
|
return groupKDB?.getChildGroups()?.map {
|
||||||
Group(it)
|
Group(it)
|
||||||
@@ -335,9 +348,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
groupKDBX?.removeChildren()
|
groupKDBX?.removeChildren()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun allowAddEntryIfIsRoot(): Boolean {
|
val allowAddEntryIfIsRoot: Boolean
|
||||||
return groupKDB?.allowAddEntryIfIsRoot() ?: groupKDBX?.allowAddEntryIfIsRoot() ?: false
|
get() = groupKDBX != null
|
||||||
}
|
|
||||||
|
val allowAddNoteInGroup: Boolean
|
||||||
|
get() = groupKDBX != null
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------
|
------------
|
||||||
@@ -391,6 +406,35 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
return groupKDBX?.containsCustomData() ?: false
|
return groupKDBX?.containsCustomData() ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
------------
|
||||||
|
Converter
|
||||||
|
------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun getGroupInfo(): GroupInfo {
|
||||||
|
val groupInfo = GroupInfo()
|
||||||
|
groupInfo.title = title
|
||||||
|
groupInfo.icon = icon
|
||||||
|
groupInfo.creationTime = creationTime
|
||||||
|
groupInfo.lastModificationTime = lastModificationTime
|
||||||
|
groupInfo.expires = expires
|
||||||
|
groupInfo.expiryTime = expiryTime
|
||||||
|
groupInfo.notes = notes
|
||||||
|
return groupInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGroupInfo(groupInfo: GroupInfo) {
|
||||||
|
title = groupInfo.title
|
||||||
|
icon = groupInfo.icon
|
||||||
|
// Update date time, creation time stay as is
|
||||||
|
lastModificationTime = DateInstant()
|
||||||
|
lastAccessTime = DateInstant()
|
||||||
|
expires = groupInfo.expires
|
||||||
|
expiryTime = groupInfo.expiryTime
|
||||||
|
notes = groupInfo.notes
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.element.database
|
||||||
|
|
||||||
|
class AttachmentPool : BinaryPool<Int>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to find an unused key in the pool
|
||||||
|
*/
|
||||||
|
override fun findUnusedKey(): Int {
|
||||||
|
var unusedKey = 0
|
||||||
|
while (pool[unusedKey] != null)
|
||||||
|
unusedKey++
|
||||||
|
return unusedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To register a binary with a ref corresponding to an ordered index
|
||||||
|
*/
|
||||||
|
fun getBinaryIndexFromKey(key: Int): Int? {
|
||||||
|
val index = orderedBinariesWithoutDuplication().indexOfFirst { it.keys.contains(key) }
|
||||||
|
return if (index < 0)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
index
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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.database.element.database
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.kunzisoft.keepass.stream.readBytes
|
|
||||||
import java.io.*
|
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import java.util.zip.GZIPOutputStream
|
|
||||||
|
|
||||||
class BinaryAttachment : Parcelable {
|
|
||||||
|
|
||||||
private var dataFile: File? = null
|
|
||||||
var isCompressed: Boolean = false
|
|
||||||
private set
|
|
||||||
var isProtected: Boolean = false
|
|
||||||
private set
|
|
||||||
var isCorrupted: Boolean = false
|
|
||||||
|
|
||||||
fun length(): Long {
|
|
||||||
return dataFile?.length() ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Empty protected binary
|
|
||||||
*/
|
|
||||||
constructor()
|
|
||||||
|
|
||||||
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
|
|
||||||
this.dataFile = dataFile
|
|
||||||
this.isCompressed = compressed
|
|
||||||
this.isProtected = protected
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(parcel: Parcel) {
|
|
||||||
parcel.readString()?.let {
|
|
||||||
dataFile = File(it)
|
|
||||||
}
|
|
||||||
isCompressed = parcel.readByte().toInt() != 0
|
|
||||||
isProtected = parcel.readByte().toInt() != 0
|
|
||||||
isCorrupted = parcel.readByte().toInt() != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getInputDataStream(): InputStream {
|
|
||||||
return when {
|
|
||||||
length() > 0 -> FileInputStream(dataFile!!)
|
|
||||||
else -> ByteArrayInputStream(ByteArray(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getUnGzipInputDataStream(): InputStream {
|
|
||||||
return if (isCompressed)
|
|
||||||
GZIPInputStream(getInputDataStream())
|
|
||||||
else
|
|
||||||
getInputDataStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getOutputDataStream(): OutputStream {
|
|
||||||
return when {
|
|
||||||
dataFile != null -> FileOutputStream(dataFile!!)
|
|
||||||
else -> throw IOException("Unable to write in an unknown file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getGzipOutputDataStream(): OutputStream {
|
|
||||||
return if (isCompressed) {
|
|
||||||
GZIPOutputStream(getOutputDataStream())
|
|
||||||
} else {
|
|
||||||
getOutputDataStream()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
|
||||||
dataFile?.let { concreteDataFile ->
|
|
||||||
// To compress, create a new binary with file
|
|
||||||
if (!isCompressed) {
|
|
||||||
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
|
||||||
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
|
|
||||||
getInputDataStream().use { inputStream ->
|
|
||||||
inputStream.readBytes(bufferSize) { buffer ->
|
|
||||||
outputStream.write(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove unGzip file
|
|
||||||
if (concreteDataFile.delete()) {
|
|
||||||
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
|
||||||
// Harmonize with database compression
|
|
||||||
isCompressed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
|
||||||
dataFile?.let { concreteDataFile ->
|
|
||||||
if (isCompressed) {
|
|
||||||
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
|
||||||
FileOutputStream(fileBinaryDecompress).use { outputStream ->
|
|
||||||
getUnGzipInputDataStream().use { inputStream ->
|
|
||||||
inputStream.readBytes(bufferSize) { buffer ->
|
|
||||||
outputStream.write(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove gzip file
|
|
||||||
if (concreteDataFile.delete()) {
|
|
||||||
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
|
||||||
// Harmonize with database compression
|
|
||||||
isCompressed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun clear() {
|
|
||||||
if (dataFile != null && !dataFile!!.delete())
|
|
||||||
throw IOException("Unable to delete temp file " + dataFile!!.absolutePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other)
|
|
||||||
return true
|
|
||||||
if (other == null || javaClass != other.javaClass)
|
|
||||||
return false
|
|
||||||
if (other !is BinaryAttachment)
|
|
||||||
return false
|
|
||||||
|
|
||||||
var sameData = false
|
|
||||||
if (dataFile != null && dataFile == other.dataFile)
|
|
||||||
sameData = true
|
|
||||||
|
|
||||||
return isCompressed == other.isCompressed
|
|
||||||
&& isProtected == other.isProtected
|
|
||||||
&& isCorrupted == other.isCorrupted
|
|
||||||
&& sameData
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
|
|
||||||
var result = 0
|
|
||||||
result = 31 * result + if (isCompressed) 1 else 0
|
|
||||||
result = 31 * result + if (isProtected) 1 else 0
|
|
||||||
result = 31 * result + if (isCorrupted) 1 else 0
|
|
||||||
result = 31 * result + dataFile!!.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return dataFile.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
|
||||||
dest.writeString(dataFile?.absolutePath)
|
|
||||||
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
|
||||||
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
|
||||||
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val TAG = BinaryAttachment::class.java.name
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val CREATOR: Parcelable.Creator<BinaryAttachment> = object : Parcelable.Creator<BinaryAttachment> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): BinaryAttachment {
|
|
||||||
return BinaryAttachment(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<BinaryAttachment?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 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.database.element.database
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.stream.readAllBytes
|
||||||
|
import java.io.*
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
class BinaryByte : BinaryData {
|
||||||
|
|
||||||
|
private var mDataByte: ByteArray = ByteArray(0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty protected binary
|
||||||
|
*/
|
||||||
|
constructor() : super()
|
||||||
|
|
||||||
|
constructor(byteArray: ByteArray,
|
||||||
|
compressed: Boolean = false,
|
||||||
|
protected: Boolean = false) : super(compressed, protected) {
|
||||||
|
this.mDataByte = byteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
|
val byteArray = ByteArray(parcel.readInt())
|
||||||
|
parcel.readByteArray(byteArray)
|
||||||
|
mDataByte = byteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
|
||||||
|
return ByteArrayInputStream(mDataByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
|
||||||
|
return ByteOutputStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun compress(cipherKey: Database.LoadedKey) {
|
||||||
|
if (!isCompressed) {
|
||||||
|
GZIPOutputStream(getOutputDataStream(cipherKey)).use { outputStream ->
|
||||||
|
getInputDataStream(cipherKey).use { inputStream ->
|
||||||
|
inputStream.readAllBytes { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isCompressed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun decompress(cipherKey: Database.LoadedKey) {
|
||||||
|
if (isCompressed) {
|
||||||
|
getUnGzipInputDataStream(cipherKey).use { inputStream ->
|
||||||
|
getOutputDataStream(cipherKey).use { outputStream ->
|
||||||
|
inputStream.readAllBytes { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isCompressed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun clear() {
|
||||||
|
mDataByte = ByteArray(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dataExists(): Boolean {
|
||||||
|
return mDataByte.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSize(): Long {
|
||||||
|
return mDataByte.size.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of the raw encrypted file in temp folder, only to compare binary data
|
||||||
|
*/
|
||||||
|
override fun binaryHash(): Int {
|
||||||
|
return if (dataExists())
|
||||||
|
mDataByte.contentHashCode()
|
||||||
|
else
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return mDataByte.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(dest, flags)
|
||||||
|
dest.writeInt(mDataByte.size)
|
||||||
|
dest.writeByteArray(mDataByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is BinaryByte) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
if (!mDataByte.contentEquals(other.mDataByte)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + mDataByte.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom OutputStream to calculate the size and hash of binary file
|
||||||
|
*/
|
||||||
|
private inner class ByteOutputStream : ByteArrayOutputStream() {
|
||||||
|
override fun close() {
|
||||||
|
mDataByte = this.toByteArray()
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = BinaryByte::class.java.name
|
||||||
|
const val MAX_BINARY_BYTES = 10240
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR: Parcelable.Creator<BinaryByte> = object : Parcelable.Creator<BinaryByte> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): BinaryByte {
|
||||||
|
return BinaryByte(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<BinaryByte?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 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.database.element.database
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
abstract class BinaryData : Parcelable {
|
||||||
|
|
||||||
|
var isCompressed: Boolean = false
|
||||||
|
protected set
|
||||||
|
var isProtected: Boolean = false
|
||||||
|
protected set
|
||||||
|
var isCorrupted: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty protected binary
|
||||||
|
*/
|
||||||
|
protected constructor()
|
||||||
|
|
||||||
|
protected constructor(compressed: Boolean = false, protected: Boolean = false) {
|
||||||
|
this.isCompressed = compressed
|
||||||
|
this.isProtected = protected
|
||||||
|
}
|
||||||
|
|
||||||
|
protected constructor(parcel: Parcel) {
|
||||||
|
isCompressed = parcel.readByte().toInt() != 0
|
||||||
|
isProtected = parcel.readByte().toInt() != 0
|
||||||
|
isCorrupted = parcel.readByte().toInt() != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getUnGzipInputDataStream(cipherKey: Database.LoadedKey): InputStream {
|
||||||
|
return if (isCompressed) {
|
||||||
|
GZIPInputStream(getInputDataStream(cipherKey))
|
||||||
|
} else {
|
||||||
|
getInputDataStream(cipherKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getGzipOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
|
||||||
|
return if (isCompressed) {
|
||||||
|
GZIPOutputStream(getOutputDataStream(cipherKey))
|
||||||
|
} else {
|
||||||
|
getOutputDataStream(cipherKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun compress(cipherKey: Database.LoadedKey)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun decompress(cipherKey: Database.LoadedKey)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun clear()
|
||||||
|
|
||||||
|
abstract fun dataExists(): Boolean
|
||||||
|
|
||||||
|
abstract fun getSize(): Long
|
||||||
|
|
||||||
|
abstract fun binaryHash(): Int
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
||||||
|
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
||||||
|
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is BinaryData) return false
|
||||||
|
|
||||||
|
if (isCompressed != other.isCompressed) return false
|
||||||
|
if (isProtected != other.isProtected) return false
|
||||||
|
if (isCorrupted != other.isCorrupted) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = isCompressed.hashCode()
|
||||||
|
result = 31 * result + isProtected.hashCode()
|
||||||
|
result = 31 * result + isCorrupted.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = BinaryData::class.java.name
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 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.database.element.database
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Base64InputStream
|
||||||
|
import android.util.Base64OutputStream
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.stream.readAllBytes
|
||||||
|
import org.apache.commons.io.output.CountingOutputStream
|
||||||
|
import java.io.*
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.CipherInputStream
|
||||||
|
import javax.crypto.CipherOutputStream
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
|
||||||
|
class BinaryFile : BinaryData {
|
||||||
|
|
||||||
|
private var mDataFile: File? = null
|
||||||
|
private var mLength: Long = 0
|
||||||
|
private var mBinaryHash = 0
|
||||||
|
// Cipher to encrypt temp file
|
||||||
|
@Transient
|
||||||
|
private var cipherEncryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
|
||||||
|
@Transient
|
||||||
|
private var cipherDecryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
|
||||||
|
|
||||||
|
constructor() : super()
|
||||||
|
|
||||||
|
constructor(dataFile: File,
|
||||||
|
compressed: Boolean = false,
|
||||||
|
protected: Boolean = false) : super(compressed, protected) {
|
||||||
|
this.mDataFile = dataFile
|
||||||
|
this.mLength = 0
|
||||||
|
this.mBinaryHash = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
|
parcel.readString()?.let {
|
||||||
|
mDataFile = File(it)
|
||||||
|
}
|
||||||
|
mLength = parcel.readLong()
|
||||||
|
mBinaryHash = parcel.readInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
|
||||||
|
return buildInputStream(mDataFile, cipherKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
|
||||||
|
return buildOutputStream(mDataFile, cipherKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun buildInputStream(file: File?, cipherKey: Database.LoadedKey): InputStream {
|
||||||
|
return when {
|
||||||
|
file != null && file.length() > 0 -> {
|
||||||
|
cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
|
||||||
|
Base64InputStream(CipherInputStream(FileInputStream(file), cipherDecryption), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
else -> ByteArrayInputStream(ByteArray(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun buildOutputStream(file: File?, cipherKey: Database.LoadedKey): OutputStream {
|
||||||
|
return when {
|
||||||
|
file != null -> {
|
||||||
|
cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
|
||||||
|
BinaryCountingOutputStream(Base64OutputStream(CipherOutputStream(FileOutputStream(file), cipherEncryption), Base64.NO_WRAP))
|
||||||
|
}
|
||||||
|
else -> throw IOException("Unable to write in an unknown file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun compress(cipherKey: Database.LoadedKey) {
|
||||||
|
mDataFile?.let { concreteDataFile ->
|
||||||
|
// To compress, create a new binary with file
|
||||||
|
if (!isCompressed) {
|
||||||
|
// Encrypt the new gzipped temp file
|
||||||
|
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
|
getInputDataStream(cipherKey).use { inputStream ->
|
||||||
|
GZIPOutputStream(buildOutputStream(fileBinaryCompress, cipherKey)).use { outputStream ->
|
||||||
|
inputStream.readAllBytes { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove ungzip file
|
||||||
|
if (concreteDataFile.delete()) {
|
||||||
|
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
||||||
|
// Harmonize with database compression
|
||||||
|
isCompressed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun decompress(cipherKey: Database.LoadedKey) {
|
||||||
|
mDataFile?.let { concreteDataFile ->
|
||||||
|
if (isCompressed) {
|
||||||
|
// Encrypt the new ungzipped temp file
|
||||||
|
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
|
getUnGzipInputDataStream(cipherKey).use { inputStream ->
|
||||||
|
buildOutputStream(fileBinaryDecompress, cipherKey).use { outputStream ->
|
||||||
|
inputStream.readAllBytes { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove gzip file
|
||||||
|
if (concreteDataFile.delete()) {
|
||||||
|
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
||||||
|
// Harmonize with database compression
|
||||||
|
isCompressed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun clear() {
|
||||||
|
if (mDataFile != null && !mDataFile!!.delete())
|
||||||
|
throw IOException("Unable to delete temp file " + mDataFile!!.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dataExists(): Boolean {
|
||||||
|
return mDataFile != null && mLength > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSize(): Long {
|
||||||
|
return mLength
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of the raw encrypted file in temp folder, only to compare binary data
|
||||||
|
*/
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
override fun binaryHash(): Int {
|
||||||
|
return mBinaryHash
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return mDataFile.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(dest, flags)
|
||||||
|
dest.writeString(mDataFile?.absolutePath)
|
||||||
|
dest.writeLong(mLength)
|
||||||
|
dest.writeInt(mBinaryHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is BinaryFile) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
return mDataFile != null && mDataFile == other.mDataFile
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + (mDataFile?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + mLength.hashCode()
|
||||||
|
result = 31 * result + mBinaryHash
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom OutputStream to calculate the size and hash of binary file
|
||||||
|
*/
|
||||||
|
private inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) {
|
||||||
|
|
||||||
|
private val mMessageDigest: MessageDigest
|
||||||
|
init {
|
||||||
|
mLength = 0
|
||||||
|
mMessageDigest = MessageDigest.getInstance("MD5")
|
||||||
|
mBinaryHash = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeWrite(n: Int) {
|
||||||
|
super.beforeWrite(n)
|
||||||
|
mLength = byteCount
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(idx: Int) {
|
||||||
|
super.write(idx)
|
||||||
|
mMessageDigest.update(idx.toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(bts: ByteArray) {
|
||||||
|
super.write(bts)
|
||||||
|
mMessageDigest.update(bts)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(bts: ByteArray, st: Int, end: Int) {
|
||||||
|
super.write(bts, st, end)
|
||||||
|
mMessageDigest.update(bts, st, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
super.close()
|
||||||
|
mLength = byteCount
|
||||||
|
val bytes = mMessageDigest.digest()
|
||||||
|
mBinaryHash = ByteBuffer.wrap(bytes).int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = BinaryFile::class.java.name
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR: Parcelable.Creator<BinaryFile> = object : Parcelable.Creator<BinaryFile> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): BinaryFile {
|
||||||
|
return BinaryFile(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<BinaryFile?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,48 +19,78 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
class BinaryPool {
|
abstract class BinaryPool<T> {
|
||||||
private val pool = LinkedHashMap<Int, BinaryAttachment>()
|
|
||||||
|
protected val pool = LinkedHashMap<T, BinaryData>()
|
||||||
|
|
||||||
|
// To build unique file id
|
||||||
|
private var creationId: String = System.currentTimeMillis().toString()
|
||||||
|
private var poolId: String = abs(javaClass.simpleName.hashCode()).toString()
|
||||||
|
private var binaryFileIncrement = 0L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To get a binary by the pool key (ref attribute in entry)
|
* To get a binary by the pool key (ref attribute in entry)
|
||||||
*/
|
*/
|
||||||
operator fun get(key: Int): BinaryAttachment? {
|
operator fun get(key: T): BinaryData? {
|
||||||
return pool[key]
|
return pool[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a new binary file not yet linked to a binary
|
||||||
|
*/
|
||||||
|
fun put(key: T? = null,
|
||||||
|
builder: (uniqueBinaryId: String) -> BinaryData): KeyBinary<T> {
|
||||||
|
binaryFileIncrement++
|
||||||
|
val newBinaryFile: BinaryData = builder("$poolId$creationId$binaryFileIncrement")
|
||||||
|
val newKey = put(key, newBinaryFile)
|
||||||
|
return KeyBinary(newBinaryFile, newKey)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
|
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
|
||||||
*/
|
*/
|
||||||
fun put(key: Int?, value: BinaryAttachment) {
|
fun put(key: T?, value: BinaryData): T {
|
||||||
if (key == null)
|
if (key == null)
|
||||||
put(value)
|
return put(value)
|
||||||
else
|
else
|
||||||
pool[key] = value
|
pool[key] = value
|
||||||
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To put a [binaryAttachment] in the pool,
|
* To put a [binaryData] in the pool,
|
||||||
* if already exists, replace the current one,
|
* if already exists, replace the current one,
|
||||||
* else add it with a new key
|
* else add it with a new key
|
||||||
*/
|
*/
|
||||||
fun put(binaryAttachment: BinaryAttachment): Int {
|
fun put(binaryData: BinaryData): T {
|
||||||
var key = findKey(binaryAttachment)
|
var key: T? = findKey(binaryData)
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
key = findUnusedKey()
|
key = findUnusedKey()
|
||||||
}
|
}
|
||||||
pool[key] = binaryAttachment
|
pool[key!!] = binaryData
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a binary from the pool with its [key], the file is not deleted
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun remove(key: T) {
|
||||||
|
pool.remove(key)
|
||||||
|
// Don't clear attachment here because a file can be used in many BinaryAttachment
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a binary from the pool, the file is not deleted
|
* Remove a binary from the pool, the file is not deleted
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun remove(binaryAttachment: BinaryAttachment) {
|
fun remove(binaryData: BinaryData) {
|
||||||
findKey(binaryAttachment)?.let {
|
findKey(binaryData)?.let {
|
||||||
pool.remove(it)
|
pool.remove(it)
|
||||||
}
|
}
|
||||||
// Don't clear attachment here because a file can be used in many BinaryAttachment
|
// Don't clear attachment here because a file can be used in many BinaryAttachment
|
||||||
@@ -69,23 +99,18 @@ class BinaryPool {
|
|||||||
/**
|
/**
|
||||||
* Utility method to find an unused key in the pool
|
* Utility method to find an unused key in the pool
|
||||||
*/
|
*/
|
||||||
private fun findUnusedKey(): Int {
|
abstract fun findUnusedKey(): T
|
||||||
var unusedKey = 0
|
|
||||||
while (pool[unusedKey] != null)
|
|
||||||
unusedKey++
|
|
||||||
return unusedKey
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return key of [binaryAttachmentToRetrieve] or null if not found
|
* Return key of [binaryDataToRetrieve] or null if not found
|
||||||
*/
|
*/
|
||||||
private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? {
|
private fun findKey(binaryDataToRetrieve: BinaryData): T? {
|
||||||
val contains = pool.containsValue(binaryAttachmentToRetrieve)
|
val contains = pool.containsValue(binaryDataToRetrieve)
|
||||||
return if (!contains)
|
return if (!contains)
|
||||||
null
|
null
|
||||||
else {
|
else {
|
||||||
for ((key, binary) in pool) {
|
for ((key, binary) in pool) {
|
||||||
if (binary == binaryAttachmentToRetrieve) {
|
if (binary == binaryDataToRetrieve) {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,46 +118,116 @@ class BinaryPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isBinaryDuplicate(binaryData: BinaryData?): Boolean {
|
||||||
|
try {
|
||||||
|
binaryData?.let {
|
||||||
|
if (it.getSize() > 0) {
|
||||||
|
val searchBinaryMD5 = it.binaryHash()
|
||||||
|
var i = 0
|
||||||
|
for ((_, binary) in pool) {
|
||||||
|
if (binary.binaryHash() == searchBinaryMD5) {
|
||||||
|
i++
|
||||||
|
if (i > 1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to check binary duplication", e)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To do an action on each binary in the pool (order is not important)
|
||||||
|
*/
|
||||||
|
private fun doForEachBinary(action: (key: T, binary: BinaryData) -> Unit,
|
||||||
|
condition: (key: T, binary: BinaryData) -> Boolean) {
|
||||||
|
for ((key, value) in pool) {
|
||||||
|
if (condition.invoke(key, value)) {
|
||||||
|
action.invoke(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doForEachBinary(action: (key: T, binary: BinaryData) -> Unit) {
|
||||||
|
doForEachBinary(action) { _, _ -> true }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to order binaries and solve index problem in database v4
|
* Utility method to order binaries and solve index problem in database v4
|
||||||
*/
|
*/
|
||||||
private fun orderedBinaries(): List<KeyBinary> {
|
protected fun orderedBinariesWithoutDuplication(condition: ((binary: BinaryData) -> Boolean) = { true })
|
||||||
val keyBinaryList = ArrayList<KeyBinary>()
|
: List<KeyBinary<T>> {
|
||||||
|
val keyBinaryList = ArrayList<KeyBinary<T>>()
|
||||||
for ((key, binary) in pool) {
|
for ((key, binary) in pool) {
|
||||||
keyBinaryList.add(KeyBinary(key, binary))
|
// Don't deduplicate
|
||||||
|
val existentBinary =
|
||||||
|
try {
|
||||||
|
if (binary.getSize() > 0) {
|
||||||
|
keyBinaryList.find {
|
||||||
|
val hash0 = it.binary.binaryHash()
|
||||||
|
val hash1 = binary.binaryHash()
|
||||||
|
hash0 != 0 && hash1 != 0 && hash0 == hash1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to check binary hash", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (existentBinary == null) {
|
||||||
|
val newKeyBinary = KeyBinary(binary, key)
|
||||||
|
if (condition.invoke(newKeyBinary.binary)) {
|
||||||
|
keyBinaryList.add(newKeyBinary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (condition.invoke(existentBinary.binary)) {
|
||||||
|
existentBinary.addKey(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return keyBinaryList
|
return keyBinaryList
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To register a binary with a ref corresponding to an ordered index
|
* Different from doForEach, provide an ordered index to each binary
|
||||||
*/
|
*/
|
||||||
fun getBinaryIndexFromKey(key: Int): Int? {
|
private fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> Unit,
|
||||||
val index = orderedBinaries().indexOfFirst { it.key == key }
|
conditionToAdd: (binary: BinaryData) -> Boolean) {
|
||||||
return if (index < 0)
|
orderedBinariesWithoutDuplication(conditionToAdd).forEach { keyBinary ->
|
||||||
null
|
action.invoke(keyBinary)
|
||||||
else
|
}
|
||||||
index
|
}
|
||||||
|
|
||||||
|
fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> Unit) {
|
||||||
|
doForEachBinaryWithoutDuplication(action, { true })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Different from doForEach, provide an ordered index to each binary
|
* Different from doForEach, provide an ordered index to each binary
|
||||||
*/
|
*/
|
||||||
fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) {
|
private fun doForEachOrderedBinaryWithoutDuplication(action: (index: Int, binary: BinaryData) -> Unit,
|
||||||
orderedBinaries().forEachIndexed(action)
|
conditionToAdd: (binary: BinaryData) -> Boolean) {
|
||||||
|
orderedBinariesWithoutDuplication(conditionToAdd).forEachIndexed { index, keyBinary ->
|
||||||
|
action.invoke(index, keyBinary.binary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun doForEachOrderedBinaryWithoutDuplication(action: (index: Int, binary: BinaryData) -> Unit) {
|
||||||
* To do an action on each binary in the pool
|
doForEachOrderedBinaryWithoutDuplication(action, { true })
|
||||||
*/
|
}
|
||||||
fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) {
|
|
||||||
pool.values.forEach { action.invoke(it) }
|
fun isEmpty(): Boolean {
|
||||||
|
return pool.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun clear() {
|
fun clear() {
|
||||||
doForEachBinary {
|
doForEachBinary { _, binary ->
|
||||||
it.clear()
|
binary.clear()
|
||||||
}
|
}
|
||||||
pool.clear()
|
pool.clear()
|
||||||
}
|
}
|
||||||
@@ -149,7 +244,20 @@ class BinaryPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility data class to order binaries
|
* Utility class to order binaries
|
||||||
*/
|
*/
|
||||||
data class KeyBinary(val key: Int, val binary: BinaryAttachment)
|
class KeyBinary<T>(val binary: BinaryData, key: T) {
|
||||||
|
val keys = HashSet<T>()
|
||||||
|
init {
|
||||||
|
addKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addKey(key: T) {
|
||||||
|
keys.add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = BinaryPool::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class CustomIconPool : BinaryPool<UUID>() {
|
||||||
|
|
||||||
|
override fun findUnusedKey(): UUID {
|
||||||
|
var newUUID = UUID.randomUUID()
|
||||||
|
while (pool.containsKey(newUUID)) {
|
||||||
|
newUUID = UUID.randomUUID()
|
||||||
|
}
|
||||||
|
return newUUID
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
|||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||||
@@ -44,7 +45,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
|
|
||||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||||
|
|
||||||
private var binaryIncrement = 0
|
// Only to generate unique file name
|
||||||
|
private var binaryPool = AttachmentPool()
|
||||||
|
|
||||||
override val version: String
|
override val version: String
|
||||||
get() = "KeePass 1"
|
get() = "KeePass 1"
|
||||||
@@ -68,7 +70,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
getGroupById(backupGroupId)
|
getGroupById(backupGroupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val kdfEngine: KdfEngine?
|
override val kdfEngine: KdfEngine
|
||||||
get() = kdfListV3[0]
|
get() = kdfListV3[0]
|
||||||
|
|
||||||
override val kdfAvailableList: List<KdfEngine>
|
override val kdfAvailableList: List<KdfEngine>
|
||||||
@@ -175,6 +177,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getStandardIcon(iconId: Int): IconImageStandard {
|
||||||
|
return this.iconsManager.getIcon(iconId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun containsCustomData(): Boolean {
|
override fun containsCustomData(): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -223,7 +229,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
// Create recycle bin
|
// Create recycle bin
|
||||||
val recycleBinGroup = createGroup().apply {
|
val recycleBinGroup = createGroup().apply {
|
||||||
title = BACKUP_FOLDER_TITLE
|
title = BACKUP_FOLDER_TITLE
|
||||||
icon = iconFactory.trashIcon
|
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
||||||
}
|
}
|
||||||
addGroupTo(recycleBinGroup, rootGroup)
|
addGroupTo(recycleBinGroup, rootGroup)
|
||||||
backupGroupId = recycleBinGroup.id
|
backupGroupId = recycleBinGroup.id
|
||||||
@@ -269,11 +275,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
addEntryTo(entry, origParent)
|
addEntryTo(entry, origParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewBinary(cacheDirectory: File): BinaryAttachment {
|
fun buildNewAttachment(cacheDirectory: File): BinaryData {
|
||||||
// Generate an unique new file
|
// Generate an unique new file
|
||||||
val fileInCache = File(cacheDirectory, binaryIncrement.toString())
|
return binaryPool.put { uniqueBinaryId ->
|
||||||
binaryIncrement++
|
val fileInCache = File(cacheDirectory, uniqueBinaryId)
|
||||||
return BinaryAttachment(fileInCache)
|
BinaryFile(fileInCache)
|
||||||
|
}.binary
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -281,7 +288,5 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
|
|
||||||
const val BACKUP_FOLDER_TITLE = "Backup"
|
const val BACKUP_FOLDER_TITLE = "Backup"
|
||||||
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
|
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
|
||||||
|
|
||||||
const val BUFFER_SIZE_BYTES = 3 * 128
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BAC
|
|||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
@@ -105,11 +106,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
var lastTopVisibleGroupUUID = UUID_ZERO
|
var lastTopVisibleGroupUUID = UUID_ZERO
|
||||||
var memoryProtection = MemoryProtectionConfig()
|
var memoryProtection = MemoryProtectionConfig()
|
||||||
val deletedObjects = ArrayList<DeletedObject>()
|
val deletedObjects = ArrayList<DeletedObject>()
|
||||||
val customIcons = ArrayList<IconImageCustom>()
|
|
||||||
val customData = HashMap<String, String>()
|
val customData = HashMap<String, String>()
|
||||||
|
|
||||||
var binaryPool = BinaryPool()
|
var binaryPool = AttachmentPool()
|
||||||
private var binaryIncrement = 0 // Unique id (don't use current time because CPU too fast)
|
|
||||||
|
|
||||||
var localizedAppName = "KeePassDX"
|
var localizedAppName = "KeePassDX"
|
||||||
|
|
||||||
@@ -126,9 +125,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
*/
|
*/
|
||||||
constructor(databaseName: String, rootName: String) {
|
constructor(databaseName: String, rootName: String) {
|
||||||
name = databaseName
|
name = databaseName
|
||||||
|
kdbxVersion = FILE_VERSION_32_3
|
||||||
val group = createGroup().apply {
|
val group = createGroup().apply {
|
||||||
title = rootName
|
title = rootName
|
||||||
icon = iconFactory.folderIcon
|
icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
|
||||||
}
|
}
|
||||||
rootGroup = group
|
rootGroup = group
|
||||||
addGroupIndex(group)
|
addGroupIndex(group)
|
||||||
@@ -210,10 +210,12 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compressAllBinaries() {
|
private fun compressAllBinaries() {
|
||||||
binaryPool.doForEachBinary { binary ->
|
binaryPool.doForEachBinary { _, binary ->
|
||||||
try {
|
try {
|
||||||
|
val cipherKey = loadedCipherKey
|
||||||
|
?: throw IOException("Unable to retrieve cipher key to compress binaries")
|
||||||
// To compress, create a new binary with file
|
// To compress, create a new binary with file
|
||||||
binary.compress(BUFFER_SIZE_BYTES)
|
binary.compress(cipherKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to compress $binary", e)
|
Log.e(TAG, "Unable to compress $binary", e)
|
||||||
}
|
}
|
||||||
@@ -221,9 +223,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun decompressAllBinaries() {
|
private fun decompressAllBinaries() {
|
||||||
binaryPool.doForEachBinary { binary ->
|
binaryPool.doForEachBinary { _, binary ->
|
||||||
try {
|
try {
|
||||||
binary.decompress(BUFFER_SIZE_BYTES)
|
val cipherKey = loadedCipherKey
|
||||||
|
?: throw IOException("Unable to retrieve cipher key to decompress binaries")
|
||||||
|
binary.decompress(cipherKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to decompress $binary", e)
|
Log.e(TAG, "Unable to decompress $binary", e)
|
||||||
}
|
}
|
||||||
@@ -302,16 +306,29 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
this.dataEngine = dataEngine
|
this.dataEngine = dataEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCustomIcons(): List<IconImageCustom> {
|
override fun getStandardIcon(iconId: Int): IconImageStandard {
|
||||||
return customIcons
|
return this.iconsManager.getIcon(iconId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addCustomIcon(customIcon: IconImageCustom) {
|
fun buildNewCustomIcon(cacheDirectory: File,
|
||||||
this.customIcons.add(customIcon)
|
customIconId: UUID? = null,
|
||||||
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
|
iconsManager.buildNewCustomIcon(cacheDirectory, customIconId, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCustomData(): Map<String, String> {
|
fun addCustomIcon(cacheDirectory: File,
|
||||||
return customData
|
customIconId: UUID? = null,
|
||||||
|
dataSize: Int,
|
||||||
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
|
iconsManager.addCustomIcon(cacheDirectory, customIconId, dataSize, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
|
||||||
|
return iconsManager.isCustomIconBinaryDuplicate(binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCustomIcon(iconUuid: UUID): IconImageCustom {
|
||||||
|
return this.iconsManager.getIcon(iconUuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putCustomData(label: String, value: String) {
|
fun putCustomData(label: String, value: String) {
|
||||||
@@ -319,7 +336,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun containsCustomData(): Boolean {
|
override fun containsCustomData(): Boolean {
|
||||||
return getCustomData().isNotEmpty()
|
return customData.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@@ -545,7 +562,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
// Create recycle bin
|
// Create recycle bin
|
||||||
val recycleBinGroup = createGroup().apply {
|
val recycleBinGroup = createGroup().apply {
|
||||||
title = resources.getString(R.string.recycle_bin)
|
title = resources.getString(R.string.recycle_bin)
|
||||||
icon = iconFactory.trashIcon
|
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
||||||
enableAutoType = false
|
enableAutoType = false
|
||||||
enableSearching = false
|
enableSearching = false
|
||||||
isExpanded = false
|
isExpanded = false
|
||||||
@@ -624,21 +641,18 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return publicCustomData.size() > 0
|
return publicCustomData.size() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewBinary(cacheDirectory: File,
|
fun buildNewAttachment(cacheDirectory: File,
|
||||||
compression: Boolean,
|
compression: Boolean,
|
||||||
protection: Boolean,
|
protection: Boolean,
|
||||||
binaryPoolId: Int? = null): BinaryAttachment {
|
binaryPoolId: Int? = null): BinaryData {
|
||||||
// New file with current time
|
return binaryPool.put(binaryPoolId) { uniqueBinaryId ->
|
||||||
val fileInCache = File(cacheDirectory, binaryIncrement.toString())
|
val fileInCache = File(cacheDirectory, uniqueBinaryId)
|
||||||
binaryIncrement++
|
BinaryFile(fileInCache, compression, protection)
|
||||||
val binaryAttachment = BinaryAttachment(fileInCache, compression, protection)
|
}.binary
|
||||||
// add attachment to pool
|
|
||||||
binaryPool.put(binaryPoolId, binaryAttachment)
|
|
||||||
return binaryAttachment
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeUnlinkedAttachment(binary: BinaryAttachment, clear: Boolean) {
|
fun removeUnlinkedAttachment(binary: BinaryData, clear: Boolean) {
|
||||||
val listBinaries = ArrayList<BinaryAttachment>()
|
val listBinaries = ArrayList<BinaryData>()
|
||||||
listBinaries.add(binary)
|
listBinaries.add(binary)
|
||||||
removeUnlinkedAttachments(listBinaries, clear)
|
removeUnlinkedAttachments(listBinaries, clear)
|
||||||
}
|
}
|
||||||
@@ -647,11 +661,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
removeUnlinkedAttachments(emptyList(), clear)
|
removeUnlinkedAttachments(emptyList(), clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeUnlinkedAttachments(binaries: List<BinaryAttachment>, clear: Boolean) {
|
private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
|
||||||
// Build binaries to remove with all binaries known
|
// Build binaries to remove with all binaries known
|
||||||
val binariesToRemove = ArrayList<BinaryAttachment>()
|
val binariesToRemove = ArrayList<BinaryData>()
|
||||||
if (binaries.isEmpty()) {
|
if (binaries.isEmpty()) {
|
||||||
binaryPool.doForEachBinary { binary ->
|
binaryPool.doForEachBinary { _, binary ->
|
||||||
binariesToRemove.add(binary)
|
binariesToRemove.add(binary)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -661,7 +675,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
|
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
|
||||||
override fun operate(node: EntryKDBX): Boolean {
|
override fun operate(node: EntryKDBX): Boolean {
|
||||||
node.getAttachments(binaryPool, true).forEach {
|
node.getAttachments(binaryPool, true).forEach {
|
||||||
binariesToRemove.remove(it.binaryAttachment)
|
binariesToRemove.remove(it.binaryData)
|
||||||
}
|
}
|
||||||
return binariesToRemove.isNotEmpty()
|
return binariesToRemove.isNotEmpty()
|
||||||
}
|
}
|
||||||
@@ -708,7 +722,5 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
|
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
|
||||||
|
|
||||||
const val BASE_64_FLAG = Base64.NO_WRAP
|
const val BASE_64_FLAG = Base64.NO_WRAP
|
||||||
|
|
||||||
const val BUFFER_SIZE_BYTES = 3 * 128
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,11 @@
|
|||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupVersioned
|
import com.kunzisoft.keepass.database.element.group.GroupVersioned
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconsManager
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
@@ -54,8 +56,13 @@ abstract class DatabaseVersioned<
|
|||||||
var finalKey: ByteArray? = null
|
var finalKey: ByteArray? = null
|
||||||
protected set
|
protected set
|
||||||
|
|
||||||
var iconFactory = IconImageFactory()
|
/**
|
||||||
protected set
|
* Cipher key generated when the database is loaded, and destroyed when the database is closed
|
||||||
|
* Can be used to temporarily store database elements
|
||||||
|
*/
|
||||||
|
var loadedCipherKey: Database.LoadedKey? = null
|
||||||
|
|
||||||
|
val iconsManager = IconsManager()
|
||||||
|
|
||||||
var changeDuplicateId = false
|
var changeDuplicateId = false
|
||||||
|
|
||||||
@@ -84,13 +91,13 @@ abstract class DatabaseVersioned<
|
|||||||
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
|
fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) {
|
||||||
masterKey = getMasterKey(key, keyInputStream)
|
masterKey = getMasterKey(key, keyfileInputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray {
|
protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
|
||||||
val fileKey = getFileKey(keyInputStream)
|
val fileKey = getFileKey(keyfileInputStream)
|
||||||
val passwordKey = getPasswordKey(key)
|
val passwordKey = getPasswordKey(key)
|
||||||
|
|
||||||
val messageDigest: MessageDigest
|
val messageDigest: MessageDigest
|
||||||
@@ -328,6 +335,8 @@ abstract class DatabaseVersioned<
|
|||||||
|
|
||||||
abstract fun rootCanContainsEntry(): Boolean
|
abstract fun rootCanContainsEntry(): Boolean
|
||||||
|
|
||||||
|
abstract fun getStandardIcon(iconId: Int): IconImageStandard
|
||||||
|
|
||||||
abstract fun containsCustomData(): Boolean
|
abstract fun containsCustomData(): Boolean
|
||||||
|
|
||||||
fun addGroupTo(newGroup: Group, parent: Group?) {
|
fun addGroupTo(newGroup: Group, parent: Group?) {
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ package com.kunzisoft.keepass.database.element.entry
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
|
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.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Structure containing information about one entry.
|
* Structure containing information about one entry.
|
||||||
@@ -56,7 +56,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
|
|
||||||
/** A string describing what is in binaryData */
|
/** A string describing what is in binaryData */
|
||||||
var binaryDescription = ""
|
var binaryDescription = ""
|
||||||
var binaryData: BinaryAttachment? = null
|
var binaryData: BinaryData? = null
|
||||||
|
|
||||||
// Determine if this is a MetaStream entry
|
// Determine if this is a MetaStream entry
|
||||||
val isMetaStream: Boolean
|
val isMetaStream: Boolean
|
||||||
@@ -68,7 +68,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
if (username.isEmpty()) return false
|
if (username.isEmpty()) return false
|
||||||
if (username != PMS_ID_USER) return false
|
if (username != PMS_ID_USER) return false
|
||||||
if (url.isEmpty()) return false
|
if (url.isEmpty()) return false
|
||||||
return if (url != PMS_ID_URL) false else icon.isMetaStreamIcon
|
if (url != PMS_ID_URL) return false
|
||||||
|
return icon.standard.id == KEY_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initNodeId(): NodeId<UUID> {
|
override fun initNodeId(): NodeId<UUID> {
|
||||||
@@ -88,7 +89,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
url = parcel.readString() ?: url
|
url = parcel.readString() ?: url
|
||||||
notes = parcel.readString() ?: notes
|
notes = parcel.readString() ?: notes
|
||||||
binaryDescription = parcel.readString() ?: binaryDescription
|
binaryDescription = parcel.readString() ?: binaryDescription
|
||||||
binaryData = parcel.readParcelable(BinaryAttachment::class.java.classLoader)
|
binaryData = parcel.readParcelable(BinaryData::class.java.classLoader)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
||||||
@@ -150,7 +151,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
|
|
||||||
fun putAttachment(attachment: Attachment) {
|
fun putAttachment(attachment: Attachment) {
|
||||||
this.binaryDescription = attachment.name
|
this.binaryDescription = attachment.name
|
||||||
this.binaryData = attachment.binaryAttachment
|
this.binaryData = attachment.binaryData
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachment(attachment: Attachment? = null) {
|
fun removeAttachment(attachment: Attachment? = null) {
|
||||||
|
|||||||
@@ -23,12 +23,9 @@ import android.os.Parcel
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryPool
|
import com.kunzisoft.keepass.database.element.database.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
@@ -48,19 +45,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
@Transient
|
@Transient
|
||||||
private var mDecodeRef = false
|
private var mDecodeRef = false
|
||||||
|
|
||||||
override var icon: IconImage
|
|
||||||
get() {
|
|
||||||
return when {
|
|
||||||
iconCustom.isUnknown -> super.icon
|
|
||||||
else -> iconCustom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
if (value is IconImageStandard)
|
|
||||||
iconCustom = IconImageCustom.UNKNOWN_ICON
|
|
||||||
super.icon = value
|
|
||||||
}
|
|
||||||
var iconCustom = IconImageCustom.UNKNOWN_ICON
|
|
||||||
var customData = LinkedHashMap<String, String>()
|
var customData = LinkedHashMap<String, String>()
|
||||||
var fields = LinkedHashMap<String, ProtectedString>()
|
var fields = LinkedHashMap<String, ProtectedString>()
|
||||||
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
|
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
|
||||||
@@ -72,7 +56,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
var additional = ""
|
var additional = ""
|
||||||
var tags = ""
|
var tags = ""
|
||||||
|
|
||||||
fun getSize(binaryPool: BinaryPool): Long {
|
fun getSize(attachmentPool: AttachmentPool): Long {
|
||||||
var size = FIXED_LENGTH_SIZE
|
var size = FIXED_LENGTH_SIZE
|
||||||
|
|
||||||
for (entry in fields.entries) {
|
for (entry in fields.entries) {
|
||||||
@@ -80,7 +64,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
size += entry.value.length().toLong()
|
size += entry.value.length().toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
size += getAttachmentsSize(binaryPool)
|
size += getAttachmentsSize(attachmentPool)
|
||||||
|
|
||||||
size += autoType.defaultSequence.length.toLong()
|
size += autoType.defaultSequence.length.toLong()
|
||||||
for ((key, value) in autoType.entrySet()) {
|
for ((key, value) in autoType.entrySet()) {
|
||||||
@@ -89,7 +73,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (entry in history) {
|
for (entry in history) {
|
||||||
size += entry.getSize(binaryPool)
|
size += entry.getSize(attachmentPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
size += overrideURL.length.toLong()
|
size += overrideURL.length.toLong()
|
||||||
@@ -103,7 +87,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
constructor() : super()
|
constructor() : super()
|
||||||
|
|
||||||
constructor(parcel: Parcel) : super(parcel) {
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
iconCustom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: iconCustom
|
|
||||||
usageCount = UnsignedLong(parcel.readLong())
|
usageCount = UnsignedLong(parcel.readLong())
|
||||||
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
||||||
customData = ParcelableUtil.readStringParcelableMap(parcel)
|
customData = ParcelableUtil.readStringParcelableMap(parcel)
|
||||||
@@ -121,7 +104,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeParcelable(iconCustom, flags)
|
|
||||||
dest.writeLong(usageCount.toKotlinLong())
|
dest.writeLong(usageCount.toKotlinLong())
|
||||||
dest.writeParcelable(locationChanged, flags)
|
dest.writeParcelable(locationChanged, flags)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, customData)
|
ParcelableUtil.writeStringParcelableMap(dest, customData)
|
||||||
@@ -143,7 +125,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
*/
|
*/
|
||||||
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
|
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
|
||||||
super.updateWith(source)
|
super.updateWith(source)
|
||||||
iconCustom = IconImageCustom(source.iconCustom)
|
|
||||||
usageCount = source.usageCount
|
usageCount = source.usageCount
|
||||||
locationChanged = DateInstant(source.locationChanged)
|
locationChanged = DateInstant(source.locationChanged)
|
||||||
// Add all custom elements in map
|
// Add all custom elements in map
|
||||||
@@ -281,16 +262,16 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
/**
|
/**
|
||||||
* It's a list because history labels can be defined multiple times
|
* It's a list because history labels can be defined multiple times
|
||||||
*/
|
*/
|
||||||
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
|
fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
|
||||||
val entryAttachmentList = ArrayList<Attachment>()
|
val entryAttachmentList = ArrayList<Attachment>()
|
||||||
for ((label, poolId) in binaries) {
|
for ((label, poolId) in binaries) {
|
||||||
binaryPool[poolId]?.let { binary ->
|
attachmentPool[poolId]?.let { binary ->
|
||||||
entryAttachmentList.add(Attachment(label, binary))
|
entryAttachmentList.add(Attachment(label, binary))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (inHistory) {
|
if (inHistory) {
|
||||||
history.forEach {
|
history.forEach {
|
||||||
entryAttachmentList.addAll(it.getAttachments(binaryPool, false))
|
entryAttachmentList.addAll(it.getAttachments(attachmentPool, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return entryAttachmentList
|
return entryAttachmentList
|
||||||
@@ -300,8 +281,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
return binaries.isNotEmpty()
|
return binaries.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
|
||||||
binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment)
|
binaries[attachment.name] = attachmentPool.put(attachment.binaryData)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachment(attachment: Attachment) {
|
fun removeAttachment(attachment: Attachment) {
|
||||||
@@ -312,11 +293,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
binaries.clear()
|
binaries.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAttachmentsSize(binaryPool: BinaryPool): Long {
|
private fun getAttachmentsSize(attachmentPool: AttachmentPool): Long {
|
||||||
var size = 0L
|
var size = 0L
|
||||||
for ((label, poolId) in binaries) {
|
for ((label, poolId) in binaries) {
|
||||||
size += label.length.toLong()
|
size += label.length.toLong()
|
||||||
size += binaryPool[poolId]?.length() ?: 0
|
size += attachmentPool[poolId]?.getSize() ?: 0
|
||||||
}
|
}
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
@@ -333,7 +314,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
history.add(entry)
|
history.add(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntryFromHistory(position: Int): EntryKDBX? {
|
fun removeEntryFromHistory(position: Int): EntryKDBX {
|
||||||
return history.removeAt(position)
|
return history.removeAt(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,10 +82,6 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
this.nodeId = NodeIdInt(groupId)
|
this.nodeId = NodeIdInt(groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun allowAddEntryIfIsRoot(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
|
|||||||
@@ -21,37 +21,18 @@ package com.kunzisoft.keepass.database.element.group
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
|
import java.util.*
|
||||||
import java.util.HashMap
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
||||||
|
|
||||||
// TODO Encapsulate
|
|
||||||
override var icon: IconImage
|
|
||||||
get() {
|
|
||||||
return if (iconCustom.isUnknown)
|
|
||||||
super.icon
|
|
||||||
else
|
|
||||||
iconCustom
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
if (value is IconImageStandard)
|
|
||||||
iconCustom = IconImageCustom.UNKNOWN_ICON
|
|
||||||
super.icon = value
|
|
||||||
}
|
|
||||||
var iconCustom = IconImageCustom.UNKNOWN_ICON
|
|
||||||
private val customData = HashMap<String, String>()
|
private val customData = HashMap<String, String>()
|
||||||
var notes = ""
|
var notes = ""
|
||||||
|
|
||||||
@@ -77,7 +58,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
constructor() : super()
|
constructor() : super()
|
||||||
|
|
||||||
constructor(parcel: Parcel) : super(parcel) {
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
iconCustom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: iconCustom
|
|
||||||
usageCount = UnsignedLong(parcel.readLong())
|
usageCount = UnsignedLong(parcel.readLong())
|
||||||
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
||||||
// TODO customData = ParcelableUtil.readStringParcelableMap(parcel);
|
// TODO customData = ParcelableUtil.readStringParcelableMap(parcel);
|
||||||
@@ -101,7 +81,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeParcelable(iconCustom, flags)
|
|
||||||
dest.writeLong(usageCount.toKotlinLong())
|
dest.writeLong(usageCount.toKotlinLong())
|
||||||
dest.writeParcelable(locationChanged, flags)
|
dest.writeParcelable(locationChanged, flags)
|
||||||
// TODO ParcelableUtil.writeStringParcelableMap(dest, customData);
|
// TODO ParcelableUtil.writeStringParcelableMap(dest, customData);
|
||||||
@@ -115,7 +94,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
|
|
||||||
fun updateWith(source: GroupKDBX) {
|
fun updateWith(source: GroupKDBX) {
|
||||||
super.updateWith(source)
|
super.updateWith(source)
|
||||||
iconCustom = IconImageCustom(source.iconCustom)
|
|
||||||
usageCount = source.usageCount
|
usageCount = source.usageCount
|
||||||
locationChanged = DateInstant(source.locationChanged)
|
locationChanged = DateInstant(source.locationChanged)
|
||||||
// Add all custom elements in map
|
// Add all custom elements in map
|
||||||
@@ -147,10 +125,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
return customData.isNotEmpty()
|
return customData.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun allowAddEntryIfIsRoot(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
|
|||||||
|
|
||||||
fun removeChildren()
|
fun removeChildren()
|
||||||
|
|
||||||
fun allowAddEntryIfIsRoot(): Boolean
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>,
|
fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>,
|
||||||
groupHandler: NodeHandler<Group>) {
|
groupHandler: NodeHandler<Group>) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -19,19 +19,69 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.icon
|
package com.kunzisoft.keepass.database.element.icon
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
|
||||||
abstract class IconImage protected constructor() : Parcelable {
|
class IconImage() : IconImageDraw(), Parcelable {
|
||||||
|
|
||||||
abstract val iconId: Int
|
var standard: IconImageStandard = IconImageStandard()
|
||||||
abstract val isUnknown: Boolean
|
var custom: IconImageCustom = IconImageCustom()
|
||||||
abstract val isMetaStreamIcon: Boolean
|
|
||||||
|
constructor(iconImageStandard: IconImageStandard) : this() {
|
||||||
|
this.standard = iconImageStandard
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(iconImageCustom: IconImageCustom) : this() {
|
||||||
|
this.custom = iconImageCustom
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(iconImageStandard: IconImageStandard,
|
||||||
|
iconImageCustom: IconImageCustom) : this() {
|
||||||
|
this.standard = iconImageStandard
|
||||||
|
this.custom = iconImageCustom
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this() {
|
||||||
|
standard = parcel.readParcelable(IconImageStandard::class.java.classLoader) ?: standard
|
||||||
|
custom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: custom
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeParcelable(standard, flags)
|
||||||
|
parcel.writeParcelable(custom, flags)
|
||||||
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
override fun getIconImageToDraw(): IconImage {
|
||||||
const val UNKNOWN_ID = -1
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is IconImage) return false
|
||||||
|
|
||||||
|
if (standard != other.standard) return false
|
||||||
|
if (custom != other.custom) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = standard.hashCode()
|
||||||
|
result = 31 * result + custom.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<IconImage> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): IconImage {
|
||||||
|
return IconImage(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<IconImage?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -22,39 +22,30 @@ package com.kunzisoft.keepass.database.element.icon
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
import java.util.UUID
|
class IconImageCustom : Parcelable, IconImageDraw {
|
||||||
|
|
||||||
class IconImageCustom : IconImage {
|
var uuid: UUID
|
||||||
|
|
||||||
val uuid: UUID
|
constructor() {
|
||||||
@Transient
|
uuid = DatabaseVersioned.UUID_ZERO
|
||||||
var imageData: ByteArray = ByteArray(0)
|
|
||||||
|
|
||||||
constructor(uuid: UUID, data: ByteArray) : super() {
|
|
||||||
this.uuid = uuid
|
|
||||||
this.imageData = data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(uuid: UUID) : super() {
|
constructor(uuid: UUID) {
|
||||||
this.uuid = uuid
|
this.uuid = uuid
|
||||||
this.imageData = ByteArray(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(icon: IconImageCustom) : super() {
|
|
||||||
uuid = icon.uuid
|
|
||||||
imageData = icon.imageData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
uuid = parcel.readSerializable() as UUID
|
uuid = parcel.readSerializable() as UUID
|
||||||
// TODO Take too much memories
|
}
|
||||||
// parcel.readByteArray(imageData);
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
dest.writeSerializable(uuid)
|
dest.writeSerializable(uuid)
|
||||||
// Too big for a parcelable dest.writeByteArray(imageData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -64,6 +55,10 @@ class IconImageCustom : IconImage {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getIconImageToDraw(): IconImage {
|
||||||
|
return IconImage(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other)
|
if (this === other)
|
||||||
return true
|
return true
|
||||||
@@ -74,17 +69,10 @@ class IconImageCustom : IconImage {
|
|||||||
return uuid == other.uuid
|
return uuid == other.uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
override val iconId: Int
|
val isUnknown: Boolean
|
||||||
get() = UNKNOWN_ID
|
get() = uuid == DatabaseVersioned.UUID_ZERO
|
||||||
|
|
||||||
override val isUnknown: Boolean
|
|
||||||
get() = this == UNKNOWN_ICON
|
|
||||||
|
|
||||||
override val isMetaStreamIcon: Boolean
|
|
||||||
get() = false
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val UNKNOWN_ICON = IconImageCustom(DatabaseVersioned.UUID_ZERO, ByteArray(0))
|
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATOR: Parcelable.Creator<IconImageCustom> = object : Parcelable.Creator<IconImageCustom> {
|
val CREATOR: Parcelable.Creator<IconImageCustom> = object : Parcelable.Creator<IconImageCustom> {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.element.icon
|
||||||
|
|
||||||
|
abstract class IconImageDraw {
|
||||||
|
|
||||||
|
var selected = false
|
||||||
|
/**
|
||||||
|
* Only to retrieve an icon image to Draw, to not use as object to manipulate
|
||||||
|
*/
|
||||||
|
abstract fun getIconImageToDraw(): IconImage
|
||||||
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Brian Pellin, 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.database.element.icon
|
|
||||||
|
|
||||||
import org.apache.commons.collections.map.AbstractReferenceMap
|
|
||||||
import org.apache.commons.collections.map.ReferenceMap
|
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
class IconImageFactory {
|
|
||||||
/** customIconMap
|
|
||||||
* Cache for icon drawable.
|
|
||||||
* Keys: Integer, Values: IconImageStandard
|
|
||||||
*/
|
|
||||||
private val cache = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
|
|
||||||
|
|
||||||
/** standardIconMap
|
|
||||||
* Cache for icon drawable.
|
|
||||||
* Keys: UUID, Values: IconImageCustom
|
|
||||||
*/
|
|
||||||
private val customCache = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
|
|
||||||
|
|
||||||
val unknownIcon: IconImageStandard
|
|
||||||
get() = getIcon(IconImage.UNKNOWN_ID)
|
|
||||||
|
|
||||||
val keyIcon: IconImageStandard
|
|
||||||
get() = getIcon(IconImageStandard.KEY)
|
|
||||||
|
|
||||||
val trashIcon: IconImageStandard
|
|
||||||
get() = getIcon(IconImageStandard.TRASH)
|
|
||||||
|
|
||||||
val folderIcon: IconImageStandard
|
|
||||||
get() = getIcon(IconImageStandard.FOLDER)
|
|
||||||
|
|
||||||
fun getIcon(iconId: Int): IconImageStandard {
|
|
||||||
var icon: IconImageStandard? = cache[iconId] as IconImageStandard?
|
|
||||||
|
|
||||||
if (icon == null) {
|
|
||||||
icon = IconImageStandard(iconId)
|
|
||||||
cache[iconId] = icon
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIcon(iconUuid: UUID): IconImageCustom {
|
|
||||||
var icon: IconImageCustom? = customCache[iconUuid] as IconImageCustom?
|
|
||||||
|
|
||||||
if (icon == null) {
|
|
||||||
icon = IconImageCustom(iconUuid)
|
|
||||||
customCache[iconUuid] = icon
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon
|
|
||||||
}
|
|
||||||
|
|
||||||
fun put(icon: IconImageCustom) {
|
|
||||||
customCache[icon.uuid] = icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,36 +21,46 @@ package com.kunzisoft.keepass.database.element.icon
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
||||||
|
|
||||||
class IconImageStandard : IconImage {
|
class IconImageStandard : Parcelable, IconImageDraw {
|
||||||
|
|
||||||
|
val id: Int
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.iconId = KEY
|
this.id = KEY_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(iconId: Int) {
|
constructor(iconId: Int) {
|
||||||
this.iconId = iconId
|
if (!isCorrectIconId(iconId))
|
||||||
}
|
this.id = KEY_ID
|
||||||
|
else
|
||||||
constructor(icon: IconImageStandard) {
|
this.id = iconId
|
||||||
this.iconId = icon.iconId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
iconId = parcel.readInt()
|
id = parcel.readInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
dest.writeInt(iconId)
|
dest.writeInt(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
val prime = 31
|
val prime = 31
|
||||||
var result = 1
|
var result = 1
|
||||||
result = prime * result + iconId
|
result = prime * result + id
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getIconImageToDraw(): IconImage {
|
||||||
|
return IconImage(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other)
|
if (this === other)
|
||||||
return true
|
return true
|
||||||
@@ -59,22 +69,18 @@ class IconImageStandard : IconImage {
|
|||||||
if (other !is IconImageStandard) {
|
if (other !is IconImageStandard) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return iconId == other.iconId
|
return id == other.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override val iconId: Int
|
|
||||||
|
|
||||||
override val isUnknown: Boolean
|
|
||||||
get() = iconId == UNKNOWN_ID
|
|
||||||
|
|
||||||
override val isMetaStreamIcon: Boolean
|
|
||||||
get() = iconId == 0
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val KEY = 0
|
const val KEY_ID = 0
|
||||||
const val TRASH = 43
|
const val TRASH_ID = 43
|
||||||
const val FOLDER = 48
|
const val FOLDER_ID = 48
|
||||||
|
|
||||||
|
fun isCorrectIconId(iconId: Int): Boolean {
|
||||||
|
return iconId in 0 until NB_ICONS
|
||||||
|
}
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATOR: Parcelable.Creator<IconImageStandard> = object : Parcelable.Creator<IconImageStandard> {
|
val CREATOR: Parcelable.Creator<IconImageStandard> = object : Parcelable.Creator<IconImageStandard> {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.element.icon
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryByte
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryByte.Companion.MAX_BINARY_BYTES
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryData
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryFile
|
||||||
|
import com.kunzisoft.keepass.database.element.database.CustomIconPool
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
|
||||||
|
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class IconsManager {
|
||||||
|
|
||||||
|
private val standardCache = List(NB_ICONS) {
|
||||||
|
IconImageStandard(it)
|
||||||
|
}
|
||||||
|
private val customCache = CustomIconPool()
|
||||||
|
|
||||||
|
fun getIcon(iconId: Int): IconImageStandard {
|
||||||
|
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
|
||||||
|
return standardCache[searchIconId]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doForEachStandardIcon(action: (IconImageStandard) -> Unit) {
|
||||||
|
standardCache.forEach { icon ->
|
||||||
|
action.invoke(icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Custom
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun buildNewCustomIcon(cacheDirectory: File,
|
||||||
|
key: UUID? = null,
|
||||||
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
|
// Create a binary file for a brand new custom icon
|
||||||
|
addCustomIcon(cacheDirectory, key, -1, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addCustomIcon(cacheDirectory: File,
|
||||||
|
key: UUID? = null,
|
||||||
|
dataSize: Int,
|
||||||
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
|
val keyBinary = customCache.put(key) { uniqueBinaryId ->
|
||||||
|
// Create a byte array for better performance with small data
|
||||||
|
if (dataSize in 1..MAX_BINARY_BYTES) {
|
||||||
|
BinaryByte()
|
||||||
|
} else {
|
||||||
|
val fileInCache = File(cacheDirectory, uniqueBinaryId)
|
||||||
|
BinaryFile(fileInCache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.invoke(IconImageCustom(keyBinary.keys.first()), keyBinary.binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getIcon(iconUuid: UUID): IconImageCustom {
|
||||||
|
return IconImageCustom(iconUuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
||||||
|
return customCache.isBinaryDuplicate(binaryData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCustomIcon(iconUuid: UUID) {
|
||||||
|
val binary = customCache[iconUuid]
|
||||||
|
customCache.remove(iconUuid)
|
||||||
|
try {
|
||||||
|
binary?.clear()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Unable to remove custom icon binary", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBinaryForCustomIcon(iconUuid: UUID): BinaryData? {
|
||||||
|
return customCache[iconUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doForEachCustomIcon(action: (IconImageCustom, BinaryData) -> Unit) {
|
||||||
|
customCache.doForEachBinary { key, binary ->
|
||||||
|
action.invoke(IconImageCustom(key), binary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the cache of icons
|
||||||
|
*/
|
||||||
|
fun clearCache() {
|
||||||
|
try {
|
||||||
|
customCache.clear()
|
||||||
|
} catch(e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to clear cache", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = IconsManager::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,11 +22,10 @@ package com.kunzisoft.keepass.database.element.node
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
|
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import org.joda.time.LocalDateTime
|
import org.joda.time.LocalDateTime
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +87,7 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
|
|||||||
|
|
||||||
final override var parent: Parent? = null
|
final override var parent: Parent? = null
|
||||||
|
|
||||||
override var icon: IconImage = IconImageStandard()
|
final override var icon: IconImage = IconImage()
|
||||||
|
|
||||||
final override var creationTime: DateInstant = DateInstant()
|
final override var creationTime: DateInstant = DateInstant()
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file.input
|
package com.kunzisoft.keepass.database.file.input
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
@@ -37,10 +38,12 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
|
|||||||
*
|
*
|
||||||
* @throws LoadDatabaseException on database error (contains IO exceptions)
|
* @throws LoadDatabaseException on database error (contains IO exceptions)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyInputStream: InputStream?,
|
keyfileInputStream: InputStream?,
|
||||||
|
loadedCipherKey: Database.LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean = false): PwDb
|
fixDuplicateUUID: Boolean = false): PwDb
|
||||||
|
|
||||||
@@ -48,6 +51,7 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||||
masterKey: ByteArray,
|
masterKey: ByteArray,
|
||||||
|
loadedCipherKey: Database.LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean = false): PwDb
|
fixDuplicateUUID: Boolean = false): PwDb
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.file.input
|
|||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
@@ -48,26 +49,30 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
class DatabaseInputKDB(cacheDirectory: File)
|
class DatabaseInputKDB(cacheDirectory: File)
|
||||||
: DatabaseInput<DatabaseKDB>(cacheDirectory) {
|
: DatabaseInput<DatabaseKDB>(cacheDirectory) {
|
||||||
|
|
||||||
private lateinit var mDatabaseToOpen: DatabaseKDB
|
private lateinit var mDatabase: DatabaseKDB
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyInputStream: InputStream?,
|
keyfileInputStream: InputStream?,
|
||||||
|
loadedCipherKey: Database.LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDB {
|
fixDuplicateUUID: Boolean): DatabaseKDB {
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
|
mDatabase.loadedCipherKey = loadedCipherKey
|
||||||
|
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
masterKey: ByteArray,
|
masterKey: ByteArray,
|
||||||
|
loadedCipherKey: Database.LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDB {
|
fixDuplicateUUID: Boolean): DatabaseKDB {
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
mDatabaseToOpen.masterKey = masterKey
|
mDatabase.loadedCipherKey = loadedCipherKey
|
||||||
|
mDatabase.masterKey = masterKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,38 +106,38 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
}
|
}
|
||||||
|
|
||||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||||
mDatabaseToOpen = DatabaseKDB()
|
mDatabase = DatabaseKDB()
|
||||||
|
|
||||||
mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID
|
mDatabase.changeDuplicateId = fixDuplicateUUID
|
||||||
assignMasterKey?.invoke()
|
assignMasterKey?.invoke()
|
||||||
|
|
||||||
// Select algorithm
|
// Select algorithm
|
||||||
when {
|
when {
|
||||||
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt() != 0 -> {
|
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt() != 0 -> {
|
||||||
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
|
mDatabase.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
|
||||||
}
|
}
|
||||||
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt() != 0 -> {
|
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt() != 0 -> {
|
||||||
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish
|
mDatabase.encryptionAlgorithm = EncryptionAlgorithm.Twofish
|
||||||
}
|
}
|
||||||
else -> throw InvalidAlgorithmDatabaseException()
|
else -> throw InvalidAlgorithmDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong()
|
mDatabase.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong()
|
||||||
|
|
||||||
// Generate transformedMasterKey from masterKey
|
// Generate transformedMasterKey from masterKey
|
||||||
mDatabaseToOpen.makeFinalKey(
|
mDatabase.makeFinalKey(
|
||||||
header.masterSeed,
|
header.masterSeed,
|
||||||
header.transformSeed,
|
header.transformSeed,
|
||||||
mDatabaseToOpen.numberKeyEncryptionRounds)
|
mDatabase.numberKeyEncryptionRounds)
|
||||||
|
|
||||||
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
|
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
|
||||||
// Initialize Rijndael algorithm
|
// Initialize Rijndael algorithm
|
||||||
val cipher: Cipher = try {
|
val cipher: Cipher = try {
|
||||||
when {
|
when {
|
||||||
mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
|
mDatabase.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
|
||||||
CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
||||||
}
|
}
|
||||||
mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
|
mDatabase.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
|
||||||
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
|
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
|
||||||
}
|
}
|
||||||
else -> throw IOException("Encryption algorithm is not supported")
|
else -> throw IOException("Encryption algorithm is not supported")
|
||||||
@@ -145,7 +150,7 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
cipher.init(Cipher.DECRYPT_MODE,
|
cipher.init(Cipher.DECRYPT_MODE,
|
||||||
SecretKeySpec(mDatabaseToOpen.finalKey, "AES"),
|
SecretKeySpec(mDatabase.finalKey, "AES"),
|
||||||
IvParameterSpec(header.encryptionIV))
|
IvParameterSpec(header.encryptionIV))
|
||||||
} catch (e1: InvalidKeyException) {
|
} catch (e1: InvalidKeyException) {
|
||||||
throw IOException("Invalid key")
|
throw IOException("Invalid key")
|
||||||
@@ -169,9 +174,10 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
)
|
)
|
||||||
|
|
||||||
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
||||||
val newRoot = mDatabaseToOpen.createGroup()
|
val newRoot = mDatabase.createGroup()
|
||||||
newRoot.level = -1
|
newRoot.level = -1
|
||||||
mDatabaseToOpen.rootGroup = newRoot
|
mDatabase.rootGroup = newRoot
|
||||||
|
mDatabase.addGroupIndex(newRoot)
|
||||||
|
|
||||||
// Import all nodes
|
// Import all nodes
|
||||||
var newGroup: GroupKDB? = null
|
var newGroup: GroupKDB? = null
|
||||||
@@ -192,12 +198,12 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
// Create new node depending on byte number
|
// Create new node depending on byte number
|
||||||
when (fieldSize) {
|
when (fieldSize) {
|
||||||
4 -> {
|
4 -> {
|
||||||
newGroup = mDatabaseToOpen.createGroup().apply {
|
newGroup = mDatabase.createGroup().apply {
|
||||||
setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt())
|
setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16 -> {
|
16 -> {
|
||||||
newEntry = mDatabaseToOpen.createEntry().apply {
|
newEntry = mDatabase.createEntry().apply {
|
||||||
nodeId = NodeIdUUID(cipherInputStream.readBytes16ToUuid())
|
nodeId = NodeIdUUID(cipherInputStream.readBytes16ToUuid())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,7 +217,7 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
group.title = cipherInputStream.readBytesToString(fieldSize)
|
group.title = cipherInputStream.readBytesToString(fieldSize)
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
val groupKDB = mDatabaseToOpen.createGroup()
|
val groupKDB = mDatabase.createGroup()
|
||||||
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toKotlinInt())
|
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toKotlinInt())
|
||||||
entry.parent = groupKDB
|
entry.parent = groupKDB
|
||||||
}
|
}
|
||||||
@@ -226,7 +232,7 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
if (iconId == -1) {
|
if (iconId == -1) {
|
||||||
iconId = 0
|
iconId = 0
|
||||||
}
|
}
|
||||||
entry.icon = mDatabaseToOpen.iconFactory.getIcon(iconId)
|
entry.icon.standard = mDatabase.getStandardIcon(iconId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0x0004 -> {
|
0x0004 -> {
|
||||||
@@ -255,7 +261,7 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
}
|
}
|
||||||
0x0007 -> {
|
0x0007 -> {
|
||||||
newGroup?.let { group ->
|
newGroup?.let { group ->
|
||||||
group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
|
group.icon.standard = mDatabase.getStandardIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.password = cipherInputStream.readBytesToString(fieldSize,false)
|
entry.password = cipherInputStream.readBytesToString(fieldSize,false)
|
||||||
@@ -300,11 +306,12 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
0x000E -> {
|
0x000E -> {
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
if (fieldSize > 0) {
|
if (fieldSize > 0) {
|
||||||
val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory)
|
val binaryAttachment = mDatabase.buildNewAttachment(cacheDirectory)
|
||||||
entry.binaryData = binaryAttachment
|
entry.binaryData = binaryAttachment
|
||||||
BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream ->
|
val cipherKey = mDatabase.loadedCipherKey
|
||||||
cipherInputStream.readBytes(fieldSize,
|
?: throw IOException("Unable to retrieve cipher key to load binaries")
|
||||||
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
|
BufferedOutputStream(binaryAttachment.getOutputDataStream(cipherKey)).use { outputStream ->
|
||||||
|
cipherInputStream.readBytes(fieldSize) { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,12 +321,12 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
0xFFFF -> {
|
0xFFFF -> {
|
||||||
// End record. Save node and count it.
|
// End record. Save node and count it.
|
||||||
newGroup?.let { group ->
|
newGroup?.let { group ->
|
||||||
mDatabaseToOpen.addGroupIndex(group)
|
mDatabase.addGroupIndex(group)
|
||||||
currentGroupNumber++
|
currentGroupNumber++
|
||||||
newGroup = null
|
newGroup = null
|
||||||
}
|
}
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
mDatabaseToOpen.addEntryIndex(entry)
|
mDatabase.addEntryIndex(entry)
|
||||||
currentEntryNumber++
|
currentEntryNumber++
|
||||||
newEntry = null
|
newEntry = null
|
||||||
}
|
}
|
||||||
@@ -337,20 +344,20 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
constructTreeFromIndex()
|
constructTreeFromIndex()
|
||||||
|
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
mDatabaseToOpen.clearCache()
|
mDatabase.clearCache()
|
||||||
throw e
|
throw e
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
mDatabaseToOpen.clearCache()
|
mDatabase.clearCache()
|
||||||
throw IODatabaseException(e)
|
throw IODatabaseException(e)
|
||||||
} catch (e: OutOfMemoryError) {
|
} catch (e: OutOfMemoryError) {
|
||||||
mDatabaseToOpen.clearCache()
|
mDatabase.clearCache()
|
||||||
throw NoMemoryDatabaseException(e)
|
throw NoMemoryDatabaseException(e)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
mDatabaseToOpen.clearCache()
|
mDatabase.clearCache()
|
||||||
throw LoadDatabaseException(e)
|
throw LoadDatabaseException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mDatabaseToOpen
|
return mDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildTreeGroups(previousGroup: GroupKDB, currentGroup: GroupKDB, groupIterator: Iterator<GroupKDB>) {
|
private fun buildTreeGroups(previousGroup: GroupKDB, currentGroup: GroupKDB, groupIterator: Iterator<GroupKDB>) {
|
||||||
@@ -375,18 +382,18 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun constructTreeFromIndex() {
|
private fun constructTreeFromIndex() {
|
||||||
mDatabaseToOpen.rootGroup?.let {
|
mDatabase.rootGroup?.let {
|
||||||
|
|
||||||
// add each group
|
// add each group
|
||||||
val groupIterator = mDatabaseToOpen.getGroupIndexes().iterator()
|
val groupIterator = mDatabase.getGroupIndexes().iterator()
|
||||||
if (groupIterator.hasNext())
|
if (groupIterator.hasNext())
|
||||||
buildTreeGroups(it, groupIterator.next(), groupIterator)
|
buildTreeGroups(it, groupIterator.next(), groupIterator)
|
||||||
|
|
||||||
// add each child
|
// add each child
|
||||||
for (currentEntry in mDatabaseToOpen.getEntryIndexes()) {
|
for (currentEntry in mDatabase.getEntryIndexes()) {
|
||||||
if (currentEntry.parent != null) {
|
if (currentEntry.parent != null) {
|
||||||
// Only the parent id is known so complete the info
|
// Only the parent id is known so complete the info
|
||||||
val parentGroupRetrieve = mDatabaseToOpen.getGroupById(currentEntry.parent!!.nodeId)
|
val parentGroupRetrieve = mDatabase.getGroupById(currentEntry.parent!!.nodeId)
|
||||||
parentGroupRetrieve?.addChildEntry(currentEntry)
|
parentGroupRetrieve?.addChildEntry(currentEntry)
|
||||||
currentEntry.parent = parentGroupRetrieve
|
currentEntry.parent = parentGroupRetrieve
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ import com.kunzisoft.keepass.crypto.CipherFactory
|
|||||||
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
import com.kunzisoft.keepass.database.element.database.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
@@ -78,7 +78,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
private var ctxStringName: String? = null
|
private var ctxStringName: String? = null
|
||||||
private var ctxStringValue: ProtectedString? = null
|
private var ctxStringValue: ProtectedString? = null
|
||||||
private var ctxBinaryName: String? = null
|
private var ctxBinaryName: String? = null
|
||||||
private var ctxBinaryValue: BinaryAttachment? = null
|
private var ctxBinaryValue: BinaryData? = null
|
||||||
private var ctxATName: String? = null
|
private var ctxATName: String? = null
|
||||||
private var ctxATSeq: String? = null
|
private var ctxATSeq: String? = null
|
||||||
private var entryInHistory = false
|
private var entryInHistory = false
|
||||||
@@ -96,20 +96,24 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyInputStream: InputStream?,
|
keyfileInputStream: InputStream?,
|
||||||
|
loadedCipherKey: Database.LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
mDatabase.retrieveMasterKey(password, keyInputStream)
|
mDatabase.loadedCipherKey = loadedCipherKey
|
||||||
|
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
masterKey: ByteArray,
|
masterKey: ByteArray,
|
||||||
|
loadedCipherKey: Database.LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
|
mDatabase.loadedCipherKey = loadedCipherKey
|
||||||
mDatabase.masterKey = masterKey
|
mDatabase.masterKey = masterKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,9 +276,11 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||||
val byteLength = size - 1
|
val byteLength = size - 1
|
||||||
// No compression at this level
|
// No compression at this level
|
||||||
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag)
|
val protectedBinary = mDatabase.buildNewAttachment(cacheDirectory, false, protectedFlag)
|
||||||
protectedBinary.getOutputDataStream().use { outputStream ->
|
val cipherKey = mDatabase.loadedCipherKey
|
||||||
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
|
?: throw IOException("Unable to retrieve cipher key to load binaries")
|
||||||
|
protectedBinary.getOutputDataStream(cipherKey).use { outputStream ->
|
||||||
|
dataInputStream.readBytes(byteLength) { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,9 +506,9 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
} else if (name.equals(DatabaseKDBXXML.ElemNotes, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemNotes, ignoreCase = true)) {
|
||||||
ctxGroup?.notes = readString(xpp)
|
ctxGroup?.notes = readString(xpp)
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
||||||
ctxGroup?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
||||||
ctxGroup?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp))
|
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
|
||||||
return switchContext(ctx, KdbContext.GroupTimes, xpp)
|
return switchContext(ctx, KdbContext.GroupTimes, xpp)
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemIsExpanded, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemIsExpanded, ignoreCase = true)) {
|
||||||
@@ -554,9 +560,9 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
KdbContext.Entry -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
KdbContext.Entry -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
||||||
ctxEntry?.nodeId = NodeIdUUID(readUuid(xpp))
|
ctxEntry?.nodeId = NodeIdUUID(readUuid(xpp))
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
||||||
ctxEntry?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
||||||
ctxEntry?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp))
|
ctxEntry?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
|
||||||
ctxEntry?.foregroundColor = readString(xpp)
|
ctxEntry?.foregroundColor = readString(xpp)
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
|
||||||
@@ -698,9 +704,13 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
return KdbContext.Meta
|
return KdbContext.Meta
|
||||||
} else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) {
|
} else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) {
|
||||||
if (customIconID != DatabaseVersioned.UUID_ZERO && customIconData != null) {
|
if (customIconID != DatabaseVersioned.UUID_ZERO && customIconData != null) {
|
||||||
val icon = IconImageCustom(customIconID, customIconData!!)
|
mDatabase.addCustomIcon(cacheDirectory, customIconID, customIconData!!.size) { _, binary ->
|
||||||
mDatabase.addCustomIcon(icon)
|
mDatabase.loadedCipherKey?.let { cipherKey ->
|
||||||
mDatabase.iconFactory.put(icon)
|
binary?.getOutputDataStream(cipherKey)?.use { outputStream ->
|
||||||
|
outputStream.write(customIconData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customIconID = DatabaseVersioned.UUID_ZERO
|
customIconID = DatabaseVersioned.UUID_ZERO
|
||||||
@@ -956,7 +966,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(XmlPullParserException::class, IOException::class)
|
@Throws(XmlPullParserException::class, IOException::class)
|
||||||
private fun readBinary(xpp: XmlPullParser): BinaryAttachment? {
|
private fun readBinary(xpp: XmlPullParser): BinaryData? {
|
||||||
|
|
||||||
// Reference Id to a binary already present in binary pool
|
// Reference Id to a binary already present in binary pool
|
||||||
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
|
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
|
||||||
@@ -971,7 +981,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
var binaryRetrieve = mDatabase.binaryPool[id]
|
var binaryRetrieve = mDatabase.binaryPool[id]
|
||||||
// Create empty binary if not retrieved in pool
|
// Create empty binary if not retrieved in pool
|
||||||
if (binaryRetrieve == null) {
|
if (binaryRetrieve == null) {
|
||||||
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory,
|
binaryRetrieve = mDatabase.buildNewAttachment(cacheDirectory,
|
||||||
compression = false, protection = false, binaryPoolId = id)
|
compression = false, protection = false, binaryPoolId = id)
|
||||||
}
|
}
|
||||||
return binaryRetrieve
|
return binaryRetrieve
|
||||||
@@ -987,7 +997,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, XmlPullParserException::class)
|
@Throws(IOException::class, XmlPullParserException::class)
|
||||||
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? {
|
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryData? {
|
||||||
var compressed = false
|
var compressed = false
|
||||||
var protected = true
|
var protected = true
|
||||||
|
|
||||||
@@ -1008,15 +1018,17 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
// Build the new binary and compress
|
// Build the new binary and compress
|
||||||
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId)
|
val binaryAttachment = mDatabase.buildNewAttachment(cacheDirectory, compressed, protected, binaryId)
|
||||||
|
val binaryCipherKey = mDatabase.loadedCipherKey
|
||||||
|
?: throw IOException("Unable to retrieve cipher key to load binaries")
|
||||||
try {
|
try {
|
||||||
binaryAttachment.getOutputDataStream().use { outputStream ->
|
binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream ->
|
||||||
outputStream.write(Base64.decode(base64, BASE_64_FLAG))
|
outputStream.write(Base64.decode(base64, BASE_64_FLAG))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to read base 64 attachment", e)
|
Log.e(TAG, "Unable to read base 64 attachment", e)
|
||||||
binaryAttachment.isCorrupted = true
|
binaryAttachment.isCorrupted = true
|
||||||
binaryAttachment.getOutputDataStream().use { outputStream ->
|
binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream ->
|
||||||
outputStream.write(base64.toByteArray())
|
outputStream.write(base64.toByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1083,6 +1095,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
return xpp
|
return xpp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, XmlPullParserException::class)
|
@Throws(IOException::class, XmlPullParserException::class)
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.file.output
|
|||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
@@ -197,15 +197,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
|
|
||||||
@Suppress("CAST_NEVER_SUCCEEDS")
|
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
fun outputPlanGroupAndEntries(os: OutputStream) {
|
fun outputPlanGroupAndEntries(outputStream: OutputStream) {
|
||||||
val los = LittleEndianDataOutputStream(os)
|
val littleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream)
|
||||||
|
|
||||||
// useHeaderHash
|
// useHeaderHash
|
||||||
if (headerHashBlock != null) {
|
if (headerHashBlock != null) {
|
||||||
try {
|
try {
|
||||||
los.writeUShort(0x0000)
|
littleEndianDataOutputStream.writeUShort(0x0000)
|
||||||
los.writeInt(headerHashBlock!!.size)
|
littleEndianDataOutputStream.writeInt(headerHashBlock!!.size)
|
||||||
los.write(headerHashBlock!!)
|
littleEndianDataOutputStream.write(headerHashBlock!!)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
throw DatabaseOutputException("Failed to output header hash.", e)
|
throw DatabaseOutputException("Failed to output header hash.", e)
|
||||||
}
|
}
|
||||||
@@ -213,20 +213,11 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
|
|
||||||
// Groups
|
// Groups
|
||||||
mDatabaseKDB.doForEachGroupInIndex { group ->
|
mDatabaseKDB.doForEachGroupInIndex { group ->
|
||||||
val pgo = GroupOutputKDB(group, os)
|
GroupOutputKDB(group, outputStream).output()
|
||||||
try {
|
|
||||||
pgo.output()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw DatabaseOutputException("Failed to output a tree", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Entries
|
||||||
mDatabaseKDB.doForEachEntryInIndex { entry ->
|
mDatabaseKDB.doForEachEntryInIndex { entry ->
|
||||||
val peo = EntryOutputKDB(entry, os)
|
EntryOutputKDB(entry, outputStream, mDatabaseKDB.loadedCipherKey).output()
|
||||||
try {
|
|
||||||
peo.output()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw DatabaseOutputException("Failed to output an entry.", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,9 @@ import com.kunzisoft.keepass.database.element.DeletedObject
|
|||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.AutoType
|
import com.kunzisoft.keepass.database.element.entry.AutoType
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
@@ -138,27 +136,28 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
dataOutputStream.writeInt(streamKeySize)
|
dataOutputStream.writeInt(streamKeySize)
|
||||||
dataOutputStream.write(header.innerRandomStreamKey)
|
dataOutputStream.write(header.innerRandomStreamKey)
|
||||||
|
|
||||||
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
|
database.loadedCipherKey?.let { binaryCipherKey ->
|
||||||
val protectedBinary = keyBinary.binary
|
database.binaryPool.doForEachOrderedBinaryWithoutDuplication { _, binary ->
|
||||||
// Force decompression to add binary in header
|
// Force decompression to add binary in header
|
||||||
protectedBinary.decompress()
|
binary.decompress(binaryCipherKey)
|
||||||
// Write type binary
|
// Write type binary
|
||||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
||||||
// Write size
|
// Write size
|
||||||
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
|
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(binary.getSize() + 1))
|
||||||
// Write protected flag
|
// Write protected flag
|
||||||
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
||||||
if (protectedBinary.isProtected) {
|
if (binary.isProtected) {
|
||||||
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||||
}
|
}
|
||||||
dataOutputStream.writeByte(flag)
|
dataOutputStream.writeByte(flag)
|
||||||
|
|
||||||
protectedBinary.getInputDataStream().use { inputStream ->
|
binary.getInputDataStream(binaryCipherKey).use { inputStream ->
|
||||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
inputStream.readAllBytes { buffer ->
|
||||||
dataOutputStream.write(buffer)
|
dataOutputStream.write(buffer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} ?: Log.e(TAG, "Unable to retrieve cipher key to write head binaries")
|
||||||
|
|
||||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
|
||||||
dataOutputStream.writeInt(0)
|
dataOutputStream.writeInt(0)
|
||||||
@@ -362,10 +361,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
writeUuid(DatabaseKDBXXML.ElemUuid, group.id)
|
writeUuid(DatabaseKDBXXML.ElemUuid, group.id)
|
||||||
writeObject(DatabaseKDBXXML.ElemName, group.title)
|
writeObject(DatabaseKDBXXML.ElemName, group.title)
|
||||||
writeObject(DatabaseKDBXXML.ElemNotes, group.notes)
|
writeObject(DatabaseKDBXXML.ElemNotes, group.notes)
|
||||||
writeObject(DatabaseKDBXXML.ElemIcon, group.icon.iconId.toLong())
|
writeObject(DatabaseKDBXXML.ElemIcon, group.icon.standard.id.toLong())
|
||||||
|
|
||||||
if (group.iconCustom != IconImageCustom.UNKNOWN_ICON) {
|
if (!group.icon.custom.isUnknown) {
|
||||||
writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.iconCustom.uuid)
|
writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.icon.custom.uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeTimes(group)
|
writeTimes(group)
|
||||||
@@ -387,10 +386,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
xml.startTag(null, DatabaseKDBXXML.ElemEntry)
|
xml.startTag(null, DatabaseKDBXXML.ElemEntry)
|
||||||
|
|
||||||
writeUuid(DatabaseKDBXXML.ElemUuid, entry.id)
|
writeUuid(DatabaseKDBXXML.ElemUuid, entry.id)
|
||||||
writeObject(DatabaseKDBXXML.ElemIcon, entry.icon.iconId.toLong())
|
writeObject(DatabaseKDBXXML.ElemIcon, entry.icon.standard.id.toLong())
|
||||||
|
|
||||||
if (entry.iconCustom != IconImageCustom.UNKNOWN_ICON) {
|
if (!entry.icon.custom.isUnknown) {
|
||||||
writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.iconCustom.uuid)
|
writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.icon.custom.uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeObject(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor)
|
writeObject(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor)
|
||||||
@@ -473,7 +472,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
if (binary.isProtected) {
|
if (binary.isProtected) {
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||||
binary.getInputDataStream().use { inputStream ->
|
binary.getInputDataStream().use { inputStream ->
|
||||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
inputStream.readBytes { buffer ->
|
||||||
val encoded = ByteArray(buffer.size)
|
val encoded = ByteArray(buffer.size)
|
||||||
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
|
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
|
||||||
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
|
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
|
||||||
@@ -482,7 +481,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
} else {
|
} else {
|
||||||
// Write the XML
|
// Write the XML
|
||||||
binary.getInputDataStream().use { inputStream ->
|
binary.getInputDataStream().use { inputStream ->
|
||||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
inputStream.readBytes { buffer ->
|
||||||
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
|
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,28 +494,31 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
// With kdbx4, don't use this method because binaries are in header file
|
// With kdbx4, don't use this method because binaries are in header file
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeMetaBinaries() {
|
private fun writeMetaBinaries() {
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
mDatabaseKDBX.loadedCipherKey?.let { binaryCipherKey ->
|
||||||
|
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||||
// Use indexes because necessarily (binary header ref is the order)
|
// Use indexes because necessarily (binary header ref is the order)
|
||||||
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
|
mDatabaseKDBX.binaryPool.doForEachOrderedBinaryWithoutDuplication { index, binary ->
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
||||||
val binary = keyBinary.binary
|
if (binary.getSize() > 0) {
|
||||||
if (binary.length() > 0) {
|
if (binary.isCompressed) {
|
||||||
if (binary.isCompressed) {
|
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
}
|
||||||
}
|
try {
|
||||||
// Write the XML
|
// Write the XML
|
||||||
binary.getInputDataStream().use { inputStream ->
|
binary.getInputDataStream(binaryCipherKey).use { inputStream ->
|
||||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
inputStream.readAllBytes { buffer ->
|
||||||
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
|
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to write binary", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
}
|
}
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
xml.endTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||||
}
|
} ?: Log.e(TAG, "Unable to retrieve cipher key to write binaries")
|
||||||
|
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemBinaries)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
@@ -589,7 +591,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
|
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
xml.text(safeXmlString(value.toString()))
|
xml.text(value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemValue)
|
xml.endTag(null, DatabaseKDBXXML.ElemValue)
|
||||||
@@ -697,38 +699,58 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeCustomIconList() {
|
private fun writeCustomIconList() {
|
||||||
val customIcons = mDatabaseKDBX.customIcons
|
mDatabaseKDBX.loadedCipherKey?.let { cipherKey ->
|
||||||
if (customIcons.size == 0) return
|
var firstElement = true
|
||||||
|
mDatabaseKDBX.iconsManager.doForEachCustomIcon { iconCustom, binary ->
|
||||||
|
if (binary.dataExists()) {
|
||||||
|
// Write the parent tag
|
||||||
|
if (firstElement) {
|
||||||
|
xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons)
|
||||||
|
firstElement = false
|
||||||
|
}
|
||||||
|
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons)
|
xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem)
|
||||||
|
|
||||||
for (icon in customIcons) {
|
writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, iconCustom.uuid)
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem)
|
var customImageData = ByteArray(0)
|
||||||
|
try {
|
||||||
|
binary.getInputDataStream(cipherKey).use { inputStream ->
|
||||||
|
customImageData = inputStream.readBytes()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to write custom icon", e)
|
||||||
|
} finally {
|
||||||
|
writeObject(DatabaseKDBXXML.ElemCustomIconItemData,
|
||||||
|
String(Base64.encode(customImageData, BASE_64_FLAG)))
|
||||||
|
}
|
||||||
|
|
||||||
writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, icon.uuid)
|
xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem)
|
||||||
writeObject(DatabaseKDBXXML.ElemCustomIconItemData, String(Base64.encode(icon.imageData, BASE_64_FLAG)))
|
}
|
||||||
|
}
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem)
|
// Close the parent tag
|
||||||
}
|
if (!firstElement) {
|
||||||
|
xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons)
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons)
|
}
|
||||||
|
} ?: Log.e(TAG, "Unable to retrieve cipher key to write custom icons")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun safeXmlString(text: String): String {
|
private fun safeXmlString(text: String): String {
|
||||||
if (text.isEmpty()) {
|
if (text.isEmpty()) {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
val stringBuilder = StringBuilder()
|
val stringBuilder = StringBuilder()
|
||||||
var ch: Char
|
var character: Char
|
||||||
for (element in text) {
|
for (element in text) {
|
||||||
ch = element
|
character = element
|
||||||
|
val hexChar = character.toInt()
|
||||||
if (
|
if (
|
||||||
ch.toInt() in 0x20..0xD7FF ||
|
hexChar in 0x20..0xD7FF ||
|
||||||
ch.toInt() == 0x9 || ch.toInt() == 0xA || ch.toInt() == 0xD ||
|
hexChar == 0x9 ||
|
||||||
ch.toInt() in 0xE000..0xFFFD
|
hexChar == 0xA ||
|
||||||
|
hexChar == 0xD ||
|
||||||
|
hexChar in 0xE000..0xFFFD
|
||||||
) {
|
) {
|
||||||
stringBuilder.append(ch)
|
stringBuilder.append(character)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stringBuilder.toString()
|
return stringBuilder.toString()
|
||||||
|
|||||||
@@ -19,9 +19,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file.output
|
package com.kunzisoft.keepass.database.file.output
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import android.util.Log
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.file.output.GroupOutputKDB.Companion.GROUPID_FIELD_SIZE
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.stream.*
|
||||||
import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils
|
import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
@@ -29,96 +30,93 @@ import java.io.IOException
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
class EntryOutputKDB
|
|
||||||
/**
|
/**
|
||||||
* Output the GroupKDB to the stream
|
* Output the GroupKDB to the stream
|
||||||
*/
|
*/
|
||||||
(private val mEntry: EntryKDB, private val mOutputStream: OutputStream) {
|
class EntryOutputKDB(private val mEntry: EntryKDB,
|
||||||
/**
|
private val mOutputStream: OutputStream,
|
||||||
* Returns the number of bytes written by the stream
|
private val mCipherKey: Database.LoadedKey?) {
|
||||||
* @return Number of bytes written
|
|
||||||
*/
|
|
||||||
var length: Long = 0
|
|
||||||
private set
|
|
||||||
|
|
||||||
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int
|
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int
|
||||||
@Throws(IOException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
fun output() {
|
fun output() {
|
||||||
|
try {
|
||||||
|
// UUID
|
||||||
|
mOutputStream.write(UUID_FIELD_TYPE)
|
||||||
|
mOutputStream.write(UUID_FIELD_SIZE)
|
||||||
|
mOutputStream.write(uuidTo16Bytes(mEntry.id))
|
||||||
|
|
||||||
length += 134 // Length of fixed size fields
|
// Group ID
|
||||||
|
mOutputStream.write(GROUPID_FIELD_TYPE)
|
||||||
|
mOutputStream.write(GROUPID_FIELD_SIZE)
|
||||||
|
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.parent!!.id)))
|
||||||
|
|
||||||
// UUID
|
// Image ID
|
||||||
mOutputStream.write(UUID_FIELD_TYPE)
|
mOutputStream.write(IMAGEID_FIELD_TYPE)
|
||||||
mOutputStream.write(UUID_FIELD_SIZE)
|
mOutputStream.write(IMAGEID_FIELD_SIZE)
|
||||||
mOutputStream.write(uuidTo16Bytes(mEntry.id))
|
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.standard.id)))
|
||||||
|
|
||||||
// Group ID
|
// Title
|
||||||
mOutputStream.write(GROUPID_FIELD_TYPE)
|
//byte[] title = mEntry.title.getBytes("UTF-8");
|
||||||
mOutputStream.write(GROUPID_FIELD_SIZE)
|
mOutputStream.write(TITLE_FIELD_TYPE)
|
||||||
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.parent!!.id)))
|
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.title)
|
||||||
|
|
||||||
// Image ID
|
// URL
|
||||||
mOutputStream.write(IMAGEID_FIELD_TYPE)
|
mOutputStream.write(URL_FIELD_TYPE)
|
||||||
mOutputStream.write(IMAGEID_FIELD_SIZE)
|
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.url)
|
||||||
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.iconId)))
|
|
||||||
|
|
||||||
// Title
|
// Username
|
||||||
//byte[] title = mEntry.title.getBytes("UTF-8");
|
mOutputStream.write(USERNAME_FIELD_TYPE)
|
||||||
mOutputStream.write(TITLE_FIELD_TYPE)
|
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.username)
|
||||||
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.title, mOutputStream).toLong()
|
|
||||||
|
|
||||||
// URL
|
// Password
|
||||||
mOutputStream.write(URL_FIELD_TYPE)
|
mOutputStream.write(PASSWORD_FIELD_TYPE)
|
||||||
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.url, mOutputStream).toLong()
|
writePassword(mEntry.password, mOutputStream)
|
||||||
|
|
||||||
// Username
|
// Additional
|
||||||
mOutputStream.write(USERNAME_FIELD_TYPE)
|
mOutputStream.write(ADDITIONAL_FIELD_TYPE)
|
||||||
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.username, mOutputStream).toLong()
|
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.notes)
|
||||||
|
|
||||||
// Password
|
// Create date
|
||||||
mOutputStream.write(PASSWORD_FIELD_TYPE)
|
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
|
||||||
length += writePassword(mEntry.password, mOutputStream).toLong()
|
|
||||||
|
|
||||||
// Additional
|
// Modification date
|
||||||
mOutputStream.write(ADDITIONAL_FIELD_TYPE)
|
writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date))
|
||||||
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.notes, mOutputStream).toLong()
|
|
||||||
|
|
||||||
// Create date
|
// Access date
|
||||||
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
|
writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date))
|
||||||
|
|
||||||
// Modification date
|
// Expiration date
|
||||||
writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date))
|
writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date))
|
||||||
|
|
||||||
// Access date
|
// Binary description
|
||||||
writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date))
|
mOutputStream.write(BINARY_DESC_FIELD_TYPE)
|
||||||
|
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.binaryDescription)
|
||||||
|
|
||||||
// Expiration date
|
// Binary
|
||||||
writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date))
|
mCipherKey?.let { cipherKey ->
|
||||||
|
mOutputStream.write(BINARY_DATA_FIELD_TYPE)
|
||||||
// Binary description
|
val binaryData = mEntry.binaryData
|
||||||
mOutputStream.write(BINARY_DESC_FIELD_TYPE)
|
val binaryDataLength = binaryData?.getSize() ?: 0L
|
||||||
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.binaryDescription, mOutputStream).toLong()
|
// Write data length
|
||||||
|
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
|
||||||
// Binary
|
// Write data
|
||||||
mOutputStream.write(BINARY_DATA_FIELD_TYPE)
|
if (binaryDataLength > 0) {
|
||||||
val binaryData = mEntry.binaryData
|
binaryData?.getInputDataStream(cipherKey).use { inputStream ->
|
||||||
val binaryDataLength = binaryData?.length() ?: 0L
|
inputStream?.readAllBytes { buffer ->
|
||||||
// Write data length
|
mOutputStream.write(buffer)
|
||||||
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
|
}
|
||||||
// Write data
|
inputStream?.close()
|
||||||
if (binaryDataLength > 0) {
|
}
|
||||||
binaryData?.getInputDataStream().use { inputStream ->
|
|
||||||
inputStream?.readBytes(DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
|
|
||||||
length += buffer.size
|
|
||||||
mOutputStream.write(buffer)
|
|
||||||
}
|
}
|
||||||
inputStream?.close()
|
} ?: Log.e(TAG, "Unable to retrieve cipher key to write entry binary")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// End
|
// End
|
||||||
mOutputStream.write(END_FIELD_TYPE)
|
mOutputStream.write(END_FIELD_TYPE)
|
||||||
mOutputStream.write(ZERO_FIELD_SIZE)
|
mOutputStream.write(ZERO_FIELD_SIZE)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw DatabaseOutputException("Failed to output an entry.", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@@ -143,30 +141,30 @@ class EntryOutputKDB
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Constants
|
|
||||||
val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1)
|
|
||||||
val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
|
|
||||||
val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
|
|
||||||
val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
|
|
||||||
val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
|
|
||||||
val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
|
|
||||||
val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
|
|
||||||
val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
|
|
||||||
val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
|
|
||||||
val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10)
|
|
||||||
val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11)
|
|
||||||
val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12)
|
|
||||||
val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13)
|
|
||||||
val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14)
|
|
||||||
val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
|
|
||||||
|
|
||||||
val LONG_FOUR:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
private val TAG = EntryOutputKDB::class.java.name
|
||||||
val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16))
|
// Constants
|
||||||
val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
|
private val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1)
|
||||||
val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
private val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
|
||||||
val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
|
||||||
val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
private val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
|
||||||
val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
|
private val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
|
||||||
val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00)
|
private val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
|
||||||
|
private val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
|
||||||
|
private val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
|
||||||
|
private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
|
||||||
|
private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10)
|
||||||
|
private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11)
|
||||||
|
private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12)
|
||||||
|
private val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13)
|
||||||
|
private val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14)
|
||||||
|
private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
|
||||||
|
|
||||||
|
private val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16))
|
||||||
|
private val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
||||||
|
private val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
|
||||||
|
private val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
||||||
|
private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
|
||||||
|
private val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.database.file.output
|
package com.kunzisoft.keepass.database.file.output
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.stream.dateTo5Bytes
|
import com.kunzisoft.keepass.stream.dateTo5Bytes
|
||||||
import com.kunzisoft.keepass.stream.uIntTo4Bytes
|
import com.kunzisoft.keepass.stream.uIntTo4Bytes
|
||||||
import com.kunzisoft.keepass.stream.uShortTo2Bytes
|
import com.kunzisoft.keepass.stream.uShortTo2Bytes
|
||||||
@@ -31,79 +32,84 @@ import java.io.OutputStream
|
|||||||
/**
|
/**
|
||||||
* Output the GroupKDB to the stream
|
* Output the GroupKDB to the stream
|
||||||
*/
|
*/
|
||||||
class GroupOutputKDB (private val mGroup: GroupKDB, private val mOutputStream: OutputStream) {
|
class GroupOutputKDB(private val mGroup: GroupKDB,
|
||||||
|
private val mOutputStream: OutputStream) {
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
fun output() {
|
fun output() {
|
||||||
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int, but most values can't be greater than 2^31, so it probably doesn't matter.
|
try {
|
||||||
|
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int, but most values can't be greater than 2^31, so it probably doesn't matter.
|
||||||
|
|
||||||
// Group ID
|
// Group ID
|
||||||
mOutputStream.write(GROUPID_FIELD_TYPE)
|
mOutputStream.write(GROUPID_FIELD_TYPE)
|
||||||
mOutputStream.write(GROUPID_FIELD_SIZE)
|
mOutputStream.write(GROUPID_FIELD_SIZE)
|
||||||
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.id)))
|
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.id)))
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
mOutputStream.write(NAME_FIELD_TYPE)
|
mOutputStream.write(NAME_FIELD_TYPE)
|
||||||
StringDatabaseKDBUtils.writeStringToBytes(mGroup.title, mOutputStream)
|
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mGroup.title)
|
||||||
|
|
||||||
// Create date
|
// Create date
|
||||||
mOutputStream.write(CREATE_FIELD_TYPE)
|
mOutputStream.write(CREATE_FIELD_TYPE)
|
||||||
mOutputStream.write(DATE_FIELD_SIZE)
|
mOutputStream.write(DATE_FIELD_SIZE)
|
||||||
mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date))
|
mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date))
|
||||||
|
|
||||||
// Modification date
|
// Modification date
|
||||||
mOutputStream.write(MOD_FIELD_TYPE)
|
mOutputStream.write(MOD_FIELD_TYPE)
|
||||||
mOutputStream.write(DATE_FIELD_SIZE)
|
mOutputStream.write(DATE_FIELD_SIZE)
|
||||||
mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date))
|
mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date))
|
||||||
|
|
||||||
// Access date
|
// Access date
|
||||||
mOutputStream.write(ACCESS_FIELD_TYPE)
|
mOutputStream.write(ACCESS_FIELD_TYPE)
|
||||||
mOutputStream.write(DATE_FIELD_SIZE)
|
mOutputStream.write(DATE_FIELD_SIZE)
|
||||||
mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date))
|
mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date))
|
||||||
|
|
||||||
// Expiration date
|
// Expiration date
|
||||||
mOutputStream.write(EXPIRE_FIELD_TYPE)
|
mOutputStream.write(EXPIRE_FIELD_TYPE)
|
||||||
mOutputStream.write(DATE_FIELD_SIZE)
|
mOutputStream.write(DATE_FIELD_SIZE)
|
||||||
mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date))
|
mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date))
|
||||||
|
|
||||||
// Image ID
|
// Image ID
|
||||||
mOutputStream.write(IMAGEID_FIELD_TYPE)
|
mOutputStream.write(IMAGEID_FIELD_TYPE)
|
||||||
mOutputStream.write(IMAGEID_FIELD_SIZE)
|
mOutputStream.write(IMAGEID_FIELD_SIZE)
|
||||||
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.iconId)))
|
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.standard.id)))
|
||||||
|
|
||||||
// Level
|
// Level
|
||||||
mOutputStream.write(LEVEL_FIELD_TYPE)
|
mOutputStream.write(LEVEL_FIELD_TYPE)
|
||||||
mOutputStream.write(LEVEL_FIELD_SIZE)
|
mOutputStream.write(LEVEL_FIELD_SIZE)
|
||||||
mOutputStream.write(uShortTo2Bytes(mGroup.level))
|
mOutputStream.write(uShortTo2Bytes(mGroup.level))
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
mOutputStream.write(FLAGS_FIELD_TYPE)
|
mOutputStream.write(FLAGS_FIELD_TYPE)
|
||||||
mOutputStream.write(FLAGS_FIELD_SIZE)
|
mOutputStream.write(FLAGS_FIELD_SIZE)
|
||||||
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.groupFlags)))
|
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.groupFlags)))
|
||||||
|
|
||||||
// End
|
// End
|
||||||
mOutputStream.write(END_FIELD_TYPE)
|
mOutputStream.write(END_FIELD_TYPE)
|
||||||
mOutputStream.write(ZERO_FIELD_SIZE)
|
mOutputStream.write(ZERO_FIELD_SIZE)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw DatabaseOutputException("Failed to output a group.", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Constants
|
// Constants
|
||||||
val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1)
|
private val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1)
|
||||||
val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
|
private val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
|
||||||
val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
|
private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
|
||||||
val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
|
private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
|
||||||
val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
|
private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
|
||||||
val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
|
private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
|
||||||
val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
|
private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
|
||||||
val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
|
private val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
|
||||||
val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
|
private val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
|
||||||
val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
|
private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
|
||||||
|
|
||||||
val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
private val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
||||||
val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
|
private val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
|
||||||
val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
private val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
||||||
val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(2))
|
private val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(2))
|
||||||
val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
private val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
|
||||||
val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
|
private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,7 @@ class SearchHelper {
|
|||||||
max: Int): Group? {
|
max: Int): Group? {
|
||||||
|
|
||||||
val searchGroup = database.createGroup()
|
val searchGroup = database.createGroup()
|
||||||
|
searchGroup?.isVirtual = true
|
||||||
searchGroup?.title = "\"" + searchQuery + "\""
|
searchGroup?.title = "\"" + searchQuery + "\""
|
||||||
|
|
||||||
// Search all entries
|
// Search all entries
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.education
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.graphics.Color
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.getkeepsafe.taptargetview.TapTarget
|
import com.getkeepsafe.taptargetview.TapTarget
|
||||||
@@ -74,6 +75,24 @@ open class Education(val activity: Activity) {
|
|||||||
editor.apply()
|
editor.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun getCircleColor(): Int {
|
||||||
|
val typedArray = activity.obtainStyledAttributes(intArrayOf(R.attr.educationCircleColor))
|
||||||
|
val colorControl = typedArray.getColor(0, Color.GREEN)
|
||||||
|
typedArray.recycle()
|
||||||
|
return colorControl
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getCircleAlpha(): Float {
|
||||||
|
return 0.98F
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getTextColor(): Int {
|
||||||
|
val typedArray = activity.obtainStyledAttributes(intArrayOf(R.attr.educationTextColor))
|
||||||
|
val colorControl = typedArray.getColor(0, Color.WHITE)
|
||||||
|
typedArray.recycle()
|
||||||
|
return colorControl
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val EDUCATION_PREFERENCE = "kdbxeducation"
|
private const val EDUCATION_PREFERENCE = "kdbxeducation"
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.education
|
package com.kunzisoft.keepass.education
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.getkeepsafe.taptargetview.TapTarget
|
import com.getkeepsafe.taptargetview.TapTarget
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView
|
import com.getkeepsafe.taptargetview.TapTargetView
|
||||||
@@ -38,7 +37,9 @@ class EntryActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_field_copy_title),
|
activity.getString(R.string.education_field_copy_title),
|
||||||
activity.getString(R.string.education_field_copy_summary))
|
activity.getString(R.string.education_field_copy_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(false)
|
.tintTarget(false)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
@@ -68,7 +69,9 @@ class EntryActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_entry_edit_title),
|
activity.getString(R.string.education_entry_edit_title),
|
||||||
activity.getString(R.string.education_entry_edit_summary))
|
activity.getString(R.string.education_entry_edit_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.education
|
package com.kunzisoft.keepass.education
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.getkeepsafe.taptargetview.TapTarget
|
import com.getkeepsafe.taptargetview.TapTarget
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView
|
import com.getkeepsafe.taptargetview.TapTargetView
|
||||||
@@ -40,7 +39,9 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_generate_password_title),
|
activity.getString(R.string.education_generate_password_title),
|
||||||
activity.getString(R.string.education_generate_password_summary))
|
activity.getString(R.string.education_generate_password_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
@@ -69,7 +70,9 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_entry_new_field_title),
|
activity.getString(R.string.education_entry_new_field_title),
|
||||||
activity.getString(R.string.education_entry_new_field_summary))
|
activity.getString(R.string.education_entry_new_field_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
@@ -98,7 +101,9 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_add_attachment_title),
|
activity.getString(R.string.education_add_attachment_title),
|
||||||
activity.getString(R.string.education_add_attachment_summary))
|
activity.getString(R.string.education_add_attachment_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
@@ -127,7 +132,9 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_setup_OTP_title),
|
activity.getString(R.string.education_setup_OTP_title),
|
||||||
activity.getString(R.string.education_setup_OTP_summary))
|
activity.getString(R.string.education_setup_OTP_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.education
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.getkeepsafe.taptargetview.TapTarget
|
import com.getkeepsafe.taptargetview.TapTarget
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView
|
import com.getkeepsafe.taptargetview.TapTargetView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -43,8 +43,10 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_create_database_title),
|
activity.getString(R.string.education_create_database_title),
|
||||||
activity.getString(R.string.education_create_database_summary))
|
activity.getString(R.string.education_create_database_summary))
|
||||||
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_database_plus_white_24dp))
|
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_database_plus_white_24dp))
|
||||||
.textColorInt(Color.WHITE)
|
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
@@ -73,8 +75,10 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_select_database_title),
|
activity.getString(R.string.education_select_database_title),
|
||||||
activity.getString(R.string.education_select_database_summary))
|
activity.getString(R.string.education_select_database_summary))
|
||||||
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_folder_white_24dp))
|
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_folder_white_24dp))
|
||||||
.textColorInt(Color.WHITE)
|
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.education
|
package com.kunzisoft.keepass.education
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.getkeepsafe.taptargetview.TapTarget
|
import com.getkeepsafe.taptargetview.TapTarget
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView
|
import com.getkeepsafe.taptargetview.TapTargetView
|
||||||
@@ -36,7 +35,9 @@ class GroupActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_new_node_title),
|
activity.getString(R.string.education_new_node_title),
|
||||||
activity.getString(R.string.education_new_node_summary))
|
activity.getString(R.string.education_new_node_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(false)
|
.tintTarget(false)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
@@ -61,7 +62,9 @@ class GroupActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_search_title),
|
activity.getString(R.string.education_search_title),
|
||||||
activity.getString(R.string.education_search_summary))
|
activity.getString(R.string.education_search_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
@@ -86,7 +89,9 @@ class GroupActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_sort_title),
|
activity.getString(R.string.education_sort_title),
|
||||||
activity.getString(R.string.education_sort_summary))
|
activity.getString(R.string.education_sort_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
@@ -111,7 +116,9 @@ class GroupActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_lock_title),
|
activity.getString(R.string.education_lock_title),
|
||||||
activity.getString(R.string.education_lock_summary))
|
activity.getString(R.string.education_lock_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.education
|
package com.kunzisoft.keepass.education
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.graphics.Color
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.getkeepsafe.taptargetview.TapTarget
|
import com.getkeepsafe.taptargetview.TapTarget
|
||||||
@@ -37,8 +36,10 @@ class PasswordActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_unlock_title),
|
activity.getString(R.string.education_unlock_title),
|
||||||
activity.getString(R.string.education_unlock_summary))
|
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.mipmap.ic_launcher_round))
|
||||||
.textColorInt(Color.WHITE)
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(false)
|
.tintTarget(false)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
@@ -63,7 +64,9 @@ class PasswordActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_read_only_title),
|
activity.getString(R.string.education_read_only_title),
|
||||||
activity.getString(R.string.education_read_only_summary))
|
activity.getString(R.string.education_read_only_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(true)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
@@ -88,7 +91,9 @@ class PasswordActivityEducation(activity: Activity)
|
|||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_advanced_unlock_title),
|
activity.getString(R.string.education_advanced_unlock_title),
|
||||||
activity.getString(R.string.education_advanced_unlock_summary))
|
activity.getString(R.string.education_advanced_unlock_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.outerCircleColorInt(getCircleColor())
|
||||||
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(false)
|
.tintTarget(false)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
|
|||||||
@@ -26,111 +26,237 @@ import android.graphics.*
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.Icon
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.core.widget.ImageViewCompat
|
import androidx.core.widget.ImageViewCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||||
import org.apache.commons.collections.map.AbstractReferenceMap
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.apache.commons.collections.map.ReferenceMap
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory class who build database icons dynamically, can assign an icon of IconPack, or a custom icon to an ImageView with a tint
|
* Factory class who build database icons dynamically, can assign an icon of IconPack, or a custom icon to an ImageView with a tint
|
||||||
*/
|
*/
|
||||||
class IconDrawableFactory {
|
class IconDrawableFactory(private val retrieveCipherKey : () -> Database.LoadedKey?,
|
||||||
|
private val retrieveCustomIconBinary : (iconId: UUID) -> BinaryData?) {
|
||||||
|
|
||||||
/** customIconMap
|
/** customIconMap
|
||||||
* Cache for icon drawable.
|
* Cache for icon drawable.
|
||||||
* Keys: UUID, Values: Drawables
|
* Keys: UUID, Values: Drawables
|
||||||
*/
|
*/
|
||||||
private val customIconMap = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
|
private val customIconMap = HashMap<UUID, WeakReference<Drawable>>()
|
||||||
|
|
||||||
/** standardIconMap
|
/** standardIconMap
|
||||||
* Cache for icon drawable.
|
* Cache for icon drawable.
|
||||||
* Keys: Integer, Values: Drawables
|
* Keys: Integer, Values: Drawables
|
||||||
*/
|
*/
|
||||||
private val standardIconMap = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
|
private val standardIconMap = HashMap<CacheKey, WeakReference<Drawable>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to assign a drawable to an ImageView and tint it
|
* Get the [SuperDrawable] [iconDraw] (from cache, or build it and add it to the cache if not exists yet), then tint it with [tintColor] if needed
|
||||||
*/
|
*/
|
||||||
fun assignDrawableToImageView(superDrawable: SuperDrawable, imageView: ImageView?, tint: Boolean, tintColor: Int) {
|
private fun getIconSuperDrawable(context: Context, iconDraw: IconImageDraw, width: Int, tintColor: Int = Color.WHITE): SuperDrawable {
|
||||||
if (imageView != null) {
|
val icon = iconDraw.getIconImageToDraw()
|
||||||
imageView.setImageDrawable(superDrawable.drawable)
|
val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid)
|
||||||
if (superDrawable.tintable && tint) {
|
if (customIconBinary != null && customIconBinary.dataExists()) {
|
||||||
ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor))
|
getIconDrawable(context.resources, icon.custom, customIconBinary)?.let {
|
||||||
|
return SuperDrawable(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val iconPack = IconPackChooser.getSelectedIconPack(context)
|
||||||
|
iconPack?.iconToResId(icon.standard.id)?.let { iconId ->
|
||||||
|
return SuperDrawable(getIconDrawable(context.resources, iconId, width, tintColor), iconPack.tintable())
|
||||||
|
} ?: run {
|
||||||
|
return SuperDrawable(PatternIcon(context.resources).blankDrawable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a custom [Drawable] from custom [icon]
|
||||||
|
*/
|
||||||
|
private fun getIconDrawable(resources: Resources, icon: IconImageCustom, iconCustomBinary: BinaryData?): Drawable? {
|
||||||
|
val patternIcon = PatternIcon(resources)
|
||||||
|
val cipherKey = retrieveCipherKey()
|
||||||
|
if (cipherKey != null) {
|
||||||
|
val draw: Drawable? = customIconMap[icon.uuid]?.get()
|
||||||
|
if (draw == null) {
|
||||||
|
iconCustomBinary?.let { binaryFile ->
|
||||||
|
try {
|
||||||
|
var bitmap: Bitmap? = BitmapFactory.decodeStream(binaryFile.getInputDataStream(cipherKey))
|
||||||
|
bitmap?.let { bitmapIcon ->
|
||||||
|
bitmap = resize(bitmapIcon, patternIcon)
|
||||||
|
val createdDraw = BitmapDrawable(resources, bitmap)
|
||||||
|
customIconMap[icon.uuid] = WeakReference(createdDraw)
|
||||||
|
return createdDraw
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
customIconMap.remove(icon.uuid)
|
||||||
|
Log.e(TAG, "Unable to create the bitmap icon", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ImageViewCompat.setImageTintList(imageView, null)
|
return draw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the standard [Drawable] icon from [iconId] (cache or build it and add it to the cache if not exists yet)
|
||||||
|
* , then tint it with [tintColor] if needed
|
||||||
|
*/
|
||||||
|
private fun getIconDrawable(resources: Resources, iconId: Int, width: Int, tintColor: Int): Drawable {
|
||||||
|
val newCacheKey = CacheKey(iconId, width, true, tintColor)
|
||||||
|
|
||||||
|
var draw: Drawable? = standardIconMap[newCacheKey]?.get()
|
||||||
|
if (draw == null) {
|
||||||
|
try {
|
||||||
|
draw = ResourcesCompat.getDrawable(resources, iconId, null)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Can't get icon", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draw != null) {
|
||||||
|
standardIconMap[newCacheKey] = WeakReference(draw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draw == null) {
|
||||||
|
draw = PatternIcon(resources).blankDrawable
|
||||||
|
}
|
||||||
|
draw.isFilterBitmap = false
|
||||||
|
|
||||||
|
return draw
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize the custom icon to match the built in icons
|
||||||
|
*
|
||||||
|
* @param bitmap Bitmap to resize
|
||||||
|
* @return Bitmap resized
|
||||||
|
*/
|
||||||
|
private fun resize(bitmap: Bitmap, dimensionPattern: PatternIcon): Bitmap {
|
||||||
|
val width = bitmap.width
|
||||||
|
val height = bitmap.height
|
||||||
|
|
||||||
|
return if (width == dimensionPattern.width && height == dimensionPattern.height) {
|
||||||
|
bitmap
|
||||||
|
} else Bitmap.createScaledBitmap(bitmap, dimensionPattern.width, dimensionPattern.height, true)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign a database [icon] to an ImageView and tint it with [tintColor] if needed
|
||||||
|
*/
|
||||||
|
fun assignDatabaseIcon(imageView: ImageView,
|
||||||
|
icon: IconImageDraw,
|
||||||
|
tintColor: Int = Color.WHITE) {
|
||||||
|
try {
|
||||||
|
val context = imageView.context
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
addToCustomCache(context.resources, icon)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val superDrawable = getIconSuperDrawable(context,
|
||||||
|
icon,
|
||||||
|
imageView.width,
|
||||||
|
tintColor)
|
||||||
|
imageView.setImageDrawable(superDrawable.drawable)
|
||||||
|
if (superDrawable.tintable) {
|
||||||
|
ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor))
|
||||||
|
} else {
|
||||||
|
ImageViewCompat.setImageTintList(imageView, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to assign a drawable to a RemoteView and tint it
|
* Build a bitmap from a database [icon]
|
||||||
*/
|
*/
|
||||||
fun assignDrawableToRemoteViews(superDrawable: SuperDrawable,
|
fun getBitmapFromIcon(context: Context,
|
||||||
remoteViews: RemoteViews,
|
icon: IconImageDraw,
|
||||||
imageId: Int,
|
tintColor: Int = Color.BLACK): Bitmap? {
|
||||||
tintColor: Int = Color.BLACK) {
|
try {
|
||||||
val bitmap = superDrawable.drawable.toBitmap()
|
val superDrawable = getIconSuperDrawable(context,
|
||||||
// Tint bitmap if it's not a custom icon
|
icon,
|
||||||
if (superDrawable.tintable && bitmap.isMutable) {
|
24,
|
||||||
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
|
tintColor)
|
||||||
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
|
val bitmap = superDrawable.drawable.toBitmap()
|
||||||
})
|
// Tint bitmap if it's not a custom icon
|
||||||
|
if (superDrawable.tintable && bitmap.isMutable) {
|
||||||
|
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
|
||||||
|
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return bitmap
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(RemoteViews::class.java.name, "Unable to create bitmap from icon", e)
|
||||||
}
|
}
|
||||||
remoteViews.setImageViewBitmap(imageId, bitmap)
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to assign a drawable to a icon and tint it
|
* Simple method to init the cache with the custom icon and be much faster next time
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
private fun addToCustomCache(resources: Resources, iconDraw: IconImageDraw) {
|
||||||
fun assignDrawableToIcon(superDrawable: SuperDrawable,
|
val icon = iconDraw.getIconImageToDraw()
|
||||||
tintColor: Int = Color.BLACK): Icon {
|
val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid)
|
||||||
val bitmap = superDrawable.drawable.toBitmap()
|
if (customIconBinary != null
|
||||||
// Tint bitmap if it's not a custom icon
|
&& customIconBinary.dataExists()
|
||||||
if (superDrawable.tintable && bitmap.isMutable) {
|
&& !customIconMap.containsKey(icon.custom.uuid))
|
||||||
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
|
getIconDrawable(resources, icon.custom, customIconBinary)
|
||||||
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return Icon.createWithBitmap(bitmap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed
|
* Clear a specific icon from the cache
|
||||||
*/
|
*/
|
||||||
fun getIconSuperDrawable(context: Context, icon: IconImage, width: Int, tint: Boolean = false, tintColor: Int = Color.WHITE): SuperDrawable {
|
fun clearFromCache(icon: IconImageCustom) {
|
||||||
return when (icon) {
|
customIconMap.remove(icon.uuid)
|
||||||
is IconImageStandard -> {
|
}
|
||||||
val resId = IconPackChooser.getSelectedIconPack(context)?.iconToResId(icon.iconId) ?: R.drawable.ic_blank_32dp
|
|
||||||
getIconSuperDrawable(context, resId, width, tint, tintColor)
|
/**
|
||||||
}
|
* Clear the cache of icons
|
||||||
is IconImageCustom -> {
|
*/
|
||||||
SuperDrawable(getIconDrawable(context.resources, icon))
|
fun clearCache() {
|
||||||
}
|
standardIconMap.clear()
|
||||||
else -> {
|
customIconMap.clear()
|
||||||
SuperDrawable(PatternIcon(context.resources).blankDrawable)
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Build a blankDrawable drawable
|
||||||
|
* @param res Resource to build the drawable
|
||||||
|
*/
|
||||||
|
private class PatternIcon(res: Resources) {
|
||||||
|
|
||||||
|
var blankDrawable: Drawable = ColorDrawable(Color.TRANSPARENT)
|
||||||
|
var width = -1
|
||||||
|
var height = -1
|
||||||
|
|
||||||
|
init {
|
||||||
|
width = res.getDimension(R.dimen.icon_size).toInt()
|
||||||
|
height = res.getDimension(R.dimen.icon_size).toInt()
|
||||||
|
blankDrawable.setBounds(0, 0, width, height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [SuperDrawable] IconImageStandard from [iconId] (cache, or build it and add it to the cache if not exists yet)
|
* Utility class to prevent a custom icon to be tint
|
||||||
* , then [tint] it with [tintColor] if needed
|
|
||||||
*/
|
*/
|
||||||
fun getIconSuperDrawable(context: Context, iconId: Int, width: Int, tint: Boolean, tintColor: Int): SuperDrawable {
|
class SuperDrawable(var drawable: Drawable, var tintable: Boolean = false)
|
||||||
return SuperDrawable(getIconDrawable(context.resources, iconId, width, tint, tintColor), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key class to retrieve a Drawable in the cache if it's tinted or not
|
* Key class to retrieve a Drawable in the cache if it's tinted or not
|
||||||
@@ -161,189 +287,9 @@ class IconDrawableFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a custom [Drawable] from custom [icon]
|
|
||||||
*/
|
|
||||||
private fun getIconDrawable(resources: Resources, icon: IconImageCustom): Drawable {
|
|
||||||
val patternIcon = PatternIcon(resources)
|
|
||||||
|
|
||||||
var draw: Drawable? = customIconMap[icon.uuid] as Drawable?
|
|
||||||
if (draw == null) {
|
|
||||||
var bitmap: Bitmap? = BitmapFactory.decodeByteArray(icon.imageData, 0, icon.imageData.size)
|
|
||||||
// Could not understand custom icon
|
|
||||||
bitmap?.let { bitmapIcon ->
|
|
||||||
bitmap = resize(bitmapIcon, patternIcon)
|
|
||||||
draw = BitmapDrawable(resources, bitmap)
|
|
||||||
customIconMap[icon.uuid] = draw
|
|
||||||
return draw!!
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return draw!!
|
|
||||||
}
|
|
||||||
return patternIcon.blankDrawable
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the standard [Drawable] icon from [iconId] (cache or build it and add it to the cache if not exists yet)
|
|
||||||
* , then [tint] it with [tintColor] if needed
|
|
||||||
*/
|
|
||||||
private fun getIconDrawable(resources: Resources, iconId: Int, width: Int, tint: Boolean, tintColor: Int): Drawable {
|
|
||||||
val newCacheKey = CacheKey(iconId, width, tint, tintColor)
|
|
||||||
|
|
||||||
var draw: Drawable? = standardIconMap[newCacheKey] as Drawable?
|
|
||||||
if (draw == null) {
|
|
||||||
try {
|
|
||||||
draw = ResourcesCompat.getDrawable(resources, iconId, null)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Can't get icon", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (draw != null) {
|
|
||||||
standardIconMap[newCacheKey] = draw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (draw == null) {
|
|
||||||
draw = PatternIcon(resources).blankDrawable
|
|
||||||
}
|
|
||||||
draw.isFilterBitmap = false
|
|
||||||
|
|
||||||
return draw
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resize the custom icon to match the built in icons
|
|
||||||
*
|
|
||||||
* @param bitmap Bitmap to resize
|
|
||||||
* @return Bitmap resized
|
|
||||||
*/
|
|
||||||
private fun resize(bitmap: Bitmap, dimensionPattern: PatternIcon): Bitmap {
|
|
||||||
val width = bitmap.width
|
|
||||||
val height = bitmap.height
|
|
||||||
|
|
||||||
return if (width == dimensionPattern.width && height == dimensionPattern.height) {
|
|
||||||
bitmap
|
|
||||||
} else Bitmap.createScaledBitmap(bitmap, dimensionPattern.width, dimensionPattern.height, true)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the cache of icons
|
|
||||||
*/
|
|
||||||
fun clearCache() {
|
|
||||||
standardIconMap.clear()
|
|
||||||
customIconMap.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PatternIcon
|
|
||||||
/**
|
|
||||||
* Build a blankDrawable drawable
|
|
||||||
* @param res Resource to build the drawable
|
|
||||||
*/(res: Resources) {
|
|
||||||
|
|
||||||
var blankDrawable: Drawable = ColorDrawable(Color.TRANSPARENT)
|
|
||||||
var width = -1
|
|
||||||
var height = -1
|
|
||||||
|
|
||||||
init {
|
|
||||||
width = res.getDimension(R.dimen.icon_size).toInt()
|
|
||||||
height = res.getDimension(R.dimen.icon_size).toInt()
|
|
||||||
blankDrawable.setBounds(0, 0, width, height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class to prevent a custom icon to be tint
|
|
||||||
*/
|
|
||||||
class SuperDrawable(var drawable: Drawable, var tintable: Boolean = false)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = IconDrawableFactory::class.java.name
|
private val TAG = IconDrawableFactory::class.java.name
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign a default database icon to an ImageView and tint it with [tintColor] if needed
|
|
||||||
*/
|
|
||||||
fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory,
|
|
||||||
tintColor: Int = Color.WHITE) {
|
|
||||||
try {
|
|
||||||
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
|
|
||||||
iconFactory.assignDrawableToImageView(
|
|
||||||
iconFactory.getIconSuperDrawable(context,
|
|
||||||
selectedIconPack.defaultIconId,
|
|
||||||
width,
|
|
||||||
selectedIconPack.tintable(),
|
|
||||||
tintColor),
|
|
||||||
this,
|
|
||||||
selectedIconPack.tintable(),
|
|
||||||
tintColor)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign a database [icon] to an ImageView and tint it with [tintColor] if needed
|
|
||||||
*/
|
|
||||||
fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory,
|
|
||||||
icon: IconImage,
|
|
||||||
tintColor: Int = Color.WHITE) {
|
|
||||||
try {
|
|
||||||
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
|
|
||||||
iconFactory.assignDrawableToImageView(
|
|
||||||
iconFactory.getIconSuperDrawable(context,
|
|
||||||
icon,
|
|
||||||
width,
|
|
||||||
true,
|
|
||||||
tintColor),
|
|
||||||
this,
|
|
||||||
selectedIconPack.tintable(),
|
|
||||||
tintColor)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun RemoteViews.assignDatabaseIcon(context: Context,
|
|
||||||
imageId: Int,
|
|
||||||
iconFactory: IconDrawableFactory,
|
|
||||||
icon: IconImage,
|
|
||||||
tintColor: Int = Color.BLACK) {
|
|
||||||
try {
|
|
||||||
iconFactory.assignDrawableToRemoteViews(
|
|
||||||
iconFactory.getIconSuperDrawable(context,
|
|
||||||
icon,
|
|
||||||
24,
|
|
||||||
true,
|
|
||||||
tintColor),
|
|
||||||
this,
|
|
||||||
imageId,
|
|
||||||
tintColor)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
fun createIconFromDatabaseIcon(context: Context,
|
|
||||||
iconFactory: IconDrawableFactory,
|
|
||||||
icon: IconImage,
|
|
||||||
tintColor: Int = Color.BLACK): Icon? {
|
|
||||||
try {
|
|
||||||
return iconFactory.assignDrawableToIcon(
|
|
||||||
iconFactory.getIconSuperDrawable(context,
|
|
||||||
icon,
|
|
||||||
24,
|
|
||||||
true,
|
|
||||||
tintColor),
|
|
||||||
tintColor)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class IconPack(packageName: String, resources: Resources, resourceId: Int) {
|
|||||||
|
|
||||||
// Build the list of icons
|
// Build the list of icons
|
||||||
var num = 0
|
var num = 0
|
||||||
while (num <= NB_ICONS) {
|
while (num < NB_ICONS) {
|
||||||
// To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp )
|
// To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp )
|
||||||
val resId = resources.getIdentifier(
|
val resId = resources.getIdentifier(
|
||||||
id + "_" + String.format(Locale.ENGLISH, "%02d", num) + "_32dp",
|
id + "_" + String.format(Locale.ENGLISH, "%02d", num) + "_32dp",
|
||||||
@@ -134,7 +134,6 @@ class IconPack(packageName: String, resources: Resources, resourceId: Int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val NB_ICONS = 69
|
||||||
private const val NB_ICONS = 68
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ object IconPackChooser {
|
|||||||
fun setSelectedIconPack(iconPackIdString: String?) {
|
fun setSelectedIconPack(iconPackIdString: String?) {
|
||||||
for (iconPack in iconPackList) {
|
for (iconPack in iconPackList) {
|
||||||
if (iconPack.id == iconPackIdString) {
|
if (iconPack.id == iconPackIdString) {
|
||||||
Database.getInstance().drawFactory.clearCache()
|
Database.getInstance().iconDrawableFactory.clearCache()
|
||||||
iconPackSelected = iconPack
|
iconPackSelected = iconPack
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import com.kunzisoft.keepass.adapters.FieldsAdapter
|
|||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.Field
|
import com.kunzisoft.keepass.model.Field
|
||||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
|
|||||||
@@ -22,26 +22,29 @@ package com.kunzisoft.keepass.model
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
import com.kunzisoft.keepass.database.element.database.BinaryByte
|
||||||
import com.kunzisoft.keepass.utils.readEnum
|
import com.kunzisoft.keepass.utils.readEnum
|
||||||
import com.kunzisoft.keepass.utils.writeEnum
|
import com.kunzisoft.keepass.utils.writeEnum
|
||||||
|
|
||||||
data class EntryAttachmentState(var attachment: Attachment,
|
data class EntryAttachmentState(var attachment: Attachment,
|
||||||
var streamDirection: StreamDirection,
|
var streamDirection: StreamDirection,
|
||||||
var downloadState: AttachmentState = AttachmentState.NULL,
|
var downloadState: AttachmentState = AttachmentState.NULL,
|
||||||
var downloadProgression: Int = 0) : Parcelable {
|
var downloadProgression: Int = 0,
|
||||||
|
var previewState: AttachmentState = AttachmentState.NULL) : Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()),
|
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryByte()),
|
||||||
parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
|
parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
|
||||||
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
|
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
|
||||||
parcel.readInt())
|
parcel.readInt(),
|
||||||
|
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL)
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeParcelable(attachment, flags)
|
parcel.writeParcelable(attachment, flags)
|
||||||
parcel.writeEnum(streamDirection)
|
parcel.writeEnum(streamDirection)
|
||||||
parcel.writeEnum(downloadState)
|
parcel.writeEnum(downloadState)
|
||||||
parcel.writeInt(downloadProgression)
|
parcel.writeInt(downloadProgression)
|
||||||
|
parcel.writeEnum(previewState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
@@ -73,5 +76,5 @@ data class EntryAttachmentState(var attachment: Attachment,
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class AttachmentState {
|
enum class AttachmentState {
|
||||||
NULL, START, IN_PROGRESS, COMPLETE, ERROR
|
NULL, START, IN_PROGRESS, COMPLETE, CANCELED, ERROR
|
||||||
}
|
}
|
||||||
@@ -23,44 +23,28 @@ import android.os.Parcel
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
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.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
class EntryInfo : Parcelable {
|
class EntryInfo : NodeInfo {
|
||||||
|
|
||||||
var id: String = ""
|
var id: String = ""
|
||||||
var title: String = ""
|
|
||||||
var icon: IconImage = IconImageStandard()
|
|
||||||
var username: String = ""
|
var username: String = ""
|
||||||
var password: String = ""
|
var password: String = ""
|
||||||
var creationTime: DateInstant = DateInstant()
|
|
||||||
var modificationTime: DateInstant = DateInstant()
|
|
||||||
var expires: Boolean = false
|
|
||||||
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
|
|
||||||
var url: String = ""
|
var url: String = ""
|
||||||
var notes: String = ""
|
var notes: String = ""
|
||||||
var customFields: List<Field> = ArrayList()
|
var customFields: List<Field> = ArrayList()
|
||||||
var attachments: List<Attachment> = ArrayList()
|
var attachments: List<Attachment> = ArrayList()
|
||||||
var otpModel: OtpModel? = null
|
var otpModel: OtpModel? = null
|
||||||
|
|
||||||
constructor()
|
constructor(): super()
|
||||||
|
|
||||||
private constructor(parcel: Parcel) {
|
constructor(parcel: Parcel): super(parcel) {
|
||||||
id = parcel.readString() ?: id
|
id = parcel.readString() ?: id
|
||||||
title = parcel.readString() ?: title
|
|
||||||
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
|
||||||
username = parcel.readString() ?: username
|
username = parcel.readString() ?: username
|
||||||
password = parcel.readString() ?: password
|
password = parcel.readString() ?: password
|
||||||
creationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: creationTime
|
|
||||||
modificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: modificationTime
|
|
||||||
expires = parcel.readInt() != 0
|
|
||||||
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
|
|
||||||
url = parcel.readString() ?: url
|
url = parcel.readString() ?: url
|
||||||
notes = parcel.readString() ?: notes
|
notes = parcel.readString() ?: notes
|
||||||
parcel.readList(customFields, Field::class.java.classLoader)
|
parcel.readList(customFields, Field::class.java.classLoader)
|
||||||
@@ -73,15 +57,10 @@ class EntryInfo : Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(parcel, flags)
|
||||||
parcel.writeString(id)
|
parcel.writeString(id)
|
||||||
parcel.writeString(title)
|
|
||||||
parcel.writeParcelable(icon, flags)
|
|
||||||
parcel.writeString(username)
|
parcel.writeString(username)
|
||||||
parcel.writeString(password)
|
parcel.writeString(password)
|
||||||
parcel.writeParcelable(creationTime, flags)
|
|
||||||
parcel.writeParcelable(modificationTime, flags)
|
|
||||||
parcel.writeInt(if (expires) 1 else 0)
|
|
||||||
parcel.writeParcelable(expiryTime, flags)
|
|
||||||
parcel.writeString(url)
|
parcel.writeString(url)
|
||||||
parcel.writeString(notes)
|
parcel.writeString(notes)
|
||||||
parcel.writeArray(customFields.toTypedArray())
|
parcel.writeArray(customFields.toTypedArray())
|
||||||
|
|||||||
36
app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt
Normal file
36
app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID
|
||||||
|
|
||||||
|
class GroupInfo : NodeInfo {
|
||||||
|
|
||||||
|
var notes: String? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
icon.standard = IconImageStandard(FOLDER_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(): super()
|
||||||
|
|
||||||
|
constructor(parcel: Parcel): super(parcel) {
|
||||||
|
notes = parcel.readString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(parcel, flags)
|
||||||
|
parcel.writeString(notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<GroupInfo> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): GroupInfo {
|
||||||
|
return GroupInfo(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<GroupInfo?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
data class MainCredential(var masterPassword: String? = null, var keyFileUri: Uri? = null): Parcelable {
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this(
|
||||||
|
parcel.readString(),
|
||||||
|
parcel.readParcelable(Uri::class.java.classLoader)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(masterPassword)
|
||||||
|
parcel.writeParcelable(keyFileUri, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<MainCredential> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): MainCredential {
|
||||||
|
return MainCredential(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<MainCredential?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt
Normal file
48
app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
|
||||||
|
open class NodeInfo() : Parcelable {
|
||||||
|
|
||||||
|
var title: String = ""
|
||||||
|
var icon: IconImage = IconImage()
|
||||||
|
var creationTime: DateInstant = DateInstant()
|
||||||
|
var lastModificationTime: DateInstant = DateInstant()
|
||||||
|
var expires: Boolean = false
|
||||||
|
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this() {
|
||||||
|
title = parcel.readString() ?: title
|
||||||
|
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
||||||
|
creationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: creationTime
|
||||||
|
lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: lastModificationTime
|
||||||
|
expires = parcel.readInt() != 0
|
||||||
|
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(title)
|
||||||
|
parcel.writeParcelable(icon, flags)
|
||||||
|
parcel.writeParcelable(creationTime, flags)
|
||||||
|
parcel.writeParcelable(lastModificationTime, flags)
|
||||||
|
parcel.writeInt(if (expires) 1 else 0)
|
||||||
|
parcel.writeParcelable(expiryTime, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<NodeInfo> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): NodeInfo {
|
||||||
|
return NodeInfo(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<NodeInfo?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user