mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
186 Commits
2.5.0.0bet
...
2.5beta27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b087733e37 | ||
|
|
675efe29ac | ||
|
|
9833af8225 | ||
|
|
790c624571 | ||
|
|
67a70a8453 | ||
|
|
64bb05e2dd | ||
|
|
c111db6e73 | ||
|
|
24c5915bd3 | ||
|
|
04c3717618 | ||
|
|
78275a0984 | ||
|
|
bd908ed10d | ||
|
|
5473deec95 | ||
|
|
653258afd2 | ||
|
|
00a2210eea | ||
|
|
ec5688a013 | ||
|
|
fc0c7b5708 | ||
|
|
464f7ac486 | ||
|
|
fb2457146c | ||
|
|
d90870838e | ||
|
|
8c45f23291 | ||
|
|
315a3efa52 | ||
|
|
28abb5ae6f | ||
|
|
bb78d89682 | ||
|
|
fd36e19168 | ||
|
|
58db516e44 | ||
|
|
2b875e94dc | ||
|
|
0ba2447f55 | ||
|
|
8e684d0d3a | ||
|
|
ce3c1d4685 | ||
|
|
2cd77d47eb | ||
|
|
ac9366e351 | ||
|
|
af356586f8 | ||
|
|
7650db81a4 | ||
|
|
8925c86afd | ||
|
|
8f1836009e | ||
|
|
5fcf3f9b95 | ||
|
|
1c466d9e40 | ||
|
|
b97c2d9cbc | ||
|
|
3d970e4967 | ||
|
|
765c8f53dd | ||
|
|
4e81caeadf | ||
|
|
08b10e4d58 | ||
|
|
f8594f72e8 | ||
|
|
24ebee07cd | ||
|
|
a06ebb0991 | ||
|
|
cd5d4498e7 | ||
|
|
0bd62780c6 | ||
|
|
896f9327d6 | ||
|
|
cc83a99efe | ||
|
|
b6da20fef7 | ||
|
|
2912678559 | ||
|
|
409e870ef0 | ||
|
|
3d84074a0a | ||
|
|
a717fdfed4 | ||
|
|
be1f68015b | ||
|
|
2c29dcf1f6 | ||
|
|
2b1173177f | ||
|
|
3c0f7dc79c | ||
|
|
39d813bf3a | ||
|
|
5e2bc0d05b | ||
|
|
ec751159ae | ||
|
|
dda8b95f83 | ||
|
|
286012fe2a | ||
|
|
ab27299789 | ||
|
|
dfcdd5aa88 | ||
|
|
a05ea52689 | ||
|
|
de12b5de5b | ||
|
|
57b03eaca4 | ||
|
|
36bd00b760 | ||
|
|
4578a9974a | ||
|
|
d0371f58c6 | ||
|
|
9f80457351 | ||
|
|
56daf6f676 | ||
|
|
7f7b8d423b | ||
|
|
031afc80cb | ||
|
|
b77e28b04d | ||
|
|
2f8c3fdcfe | ||
|
|
f501a87099 | ||
|
|
d0ec5f26dd | ||
|
|
97776e9329 | ||
|
|
a19356c49e | ||
|
|
4f16918cf0 | ||
|
|
1af9761144 | ||
|
|
d74e814c79 | ||
|
|
16d09bca6c | ||
|
|
462c29b769 | ||
|
|
3939276d58 | ||
|
|
ff85f18c4c | ||
|
|
dd14fe4123 | ||
|
|
d804659ee2 | ||
|
|
e3152cf901 | ||
|
|
de236f321f | ||
|
|
66f4611866 | ||
|
|
6a6ef052af | ||
|
|
4e4606dabd | ||
|
|
0ce11103ab | ||
|
|
899d0e0557 | ||
|
|
4aefeff41f | ||
|
|
08d59e50e8 | ||
|
|
e6c06aba8c | ||
|
|
e5c2a04922 | ||
|
|
c134ccf8d9 | ||
|
|
4f959d1ff6 | ||
|
|
1c06d93951 | ||
|
|
b2dff29baa | ||
|
|
72633e9472 | ||
|
|
898a88f7d8 | ||
|
|
c2f09f6666 | ||
|
|
d45bcbc347 | ||
|
|
1f834567f8 | ||
|
|
0b62c04867 | ||
|
|
c71bb2a570 | ||
|
|
0a575b5bf8 | ||
|
|
6f2bf903c5 | ||
|
|
210ad4b8db | ||
|
|
b42cf0e204 | ||
|
|
2045d3aab8 | ||
|
|
e20cb9431d | ||
|
|
7161eaea8b | ||
|
|
b786da52f5 | ||
|
|
ccbfec838d | ||
|
|
f5c1872225 | ||
|
|
3001013bad | ||
|
|
1280803e5e | ||
|
|
f84fc07fe0 | ||
|
|
d83e53f589 | ||
|
|
52ba2c53f1 | ||
|
|
f266e1de4c | ||
|
|
5a0aafd3ce | ||
|
|
c7ce07a43c | ||
|
|
a73644c285 | ||
|
|
e28f1ffc99 | ||
|
|
afc691b2f4 | ||
|
|
00f6eb83c3 | ||
|
|
bbf51ebbec | ||
|
|
7816c8f16e | ||
|
|
fdbcba2412 | ||
|
|
50dc6cb0aa | ||
|
|
e413198d12 | ||
|
|
72592772f9 | ||
|
|
f918206bcd | ||
|
|
6b34d67da8 | ||
|
|
04fb093d4d | ||
|
|
7de0e0cc4a | ||
|
|
5cf7688e38 | ||
|
|
97cd06b52b | ||
|
|
6d522ead1d | ||
|
|
8e0fae62f3 | ||
|
|
af72098d60 | ||
|
|
62da582f4e | ||
|
|
f3efa6eddc | ||
|
|
c85fb7bd0a | ||
|
|
fc7b183461 | ||
|
|
a378810f88 | ||
|
|
2f5b322fad | ||
|
|
efb9b50f85 | ||
|
|
84fdef8eb6 | ||
|
|
76050a261b | ||
|
|
2667361450 | ||
|
|
a2b3313cc0 | ||
|
|
d343446235 | ||
|
|
cbce70f8a4 | ||
|
|
4573d6b6fb | ||
|
|
973305d13c | ||
|
|
4637016372 | ||
|
|
698ba4f7f1 | ||
|
|
502ebabf1f | ||
|
|
13f88626ca | ||
|
|
a39f58f7b5 | ||
|
|
aeb36468ce | ||
|
|
fdd196526d | ||
|
|
ecca892b16 | ||
|
|
03343d0301 | ||
|
|
a61c8b0cb6 | ||
|
|
a92129da00 | ||
|
|
0b783d6390 | ||
|
|
9aa1a2e30e | ||
|
|
15b3c69514 | ||
|
|
f2722e09ec | ||
|
|
b442859be0 | ||
|
|
1496a2efb1 | ||
|
|
a0edb111f0 | ||
|
|
6bcf54fe29 | ||
|
|
eb0e496cfd | ||
|
|
12c2a6e99c | ||
|
|
714433b62d |
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -20,9 +20,6 @@ Steps to reproduce the behavior:
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
** Keepass Database **
|
||||
- Created with: [e.g Windows KeePass 2.42]
|
||||
- Version: [e.g. 2]
|
||||
|
||||
22
CHANGELOG
22
CHANGELOG
@@ -1,3 +1,25 @@
|
||||
KeepassDX (2.5beta27)
|
||||
* New setting to hide broken links
|
||||
* Show URL when title is empty
|
||||
* Setting to open search field at database opening
|
||||
* Fix settings for database locations
|
||||
* Fix error message when database file not writable
|
||||
* Fix appearance refresh settings
|
||||
* Sort optimization
|
||||
|
||||
KeepassDX (2.5.0.0beta26)
|
||||
* Download attachments
|
||||
* Change file size string format
|
||||
* Prevent screenshot for all screen
|
||||
* Auto performed "Go" key in Magikeyboard
|
||||
* Restore and delete entry history
|
||||
* Setting to hide expired entries
|
||||
* New Black theme
|
||||
* Fix crash when clearing clipboard
|
||||
* Fix attachments compressions
|
||||
* Fix dates
|
||||
* Fix UUID message for Database v1
|
||||
|
||||
KeepassDX (2.5.0.0beta25)
|
||||
* Setting for Recycle Bin
|
||||
* Fix Recycle bin issues
|
||||
|
||||
10
ReadMe.md
10
ReadMe.md
@@ -11,7 +11,7 @@
|
||||
* **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC...)
|
||||
* Allows **fast copy** of fields and opening of URI / URL
|
||||
* **Biometric recognition** for fast unlocking *(Fingerprint / Face unlock / ...)*
|
||||
* **One-Time Password** management *(HOTP / TOTP)*
|
||||
* **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA)
|
||||
* Material design with **themes**
|
||||
* **AutoFill** and Integration
|
||||
* Field filling **keyboard**
|
||||
@@ -59,13 +59,15 @@ Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassD
|
||||
|
||||
## Other devices
|
||||
|
||||
- [KeePass XC](https://keepassxc.org/) (https://keepassxc.org/) works with **GNU/Linux**, **Mac** and **Windows**, is updated regularly and under the terms of the GNU General Public License. This is the recommended version for computers.
|
||||
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
|
||||
|
||||
- [KeePass](https://keepass.info/) (https://keepass.info/) is the historical project, with good technical documentation for standardized database files but only running on **Windows**.
|
||||
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration to KeePass written in C++.
|
||||
|
||||
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version also compatible with KeePass files.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2019 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
Copyright (c) 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 28
|
||||
versionCode = 25
|
||||
versionName = "2.5.0.0beta25"
|
||||
versionCode = 27
|
||||
versionName = "2.5beta27"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
import org.junit.Assert
|
||||
|
||||
class PwDateTest : TestCase() {
|
||||
|
||||
fun testDate() {
|
||||
val jDate = DateInstant(System.currentTimeMillis())
|
||||
val intermediate = DateInstant(jDate)
|
||||
val cDate = DatabaseInputOutputUtils.readCDate(DatabaseInputOutputUtils.writeCDate(intermediate.date)!!, 0)
|
||||
|
||||
Assert.assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
||||
Assert.assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
@@ -19,19 +19,15 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.Calendar
|
||||
import java.util.Random
|
||||
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.ULONG_MAX_VALUE
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import junit.framework.TestCase
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
|
||||
class DatabaseInputOutputUtilsTest : TestCase() {
|
||||
class StringDatabaseKDBUtilsTest : TestCase() {
|
||||
|
||||
fun testReadWriteLongZero() {
|
||||
testReadWriteLong(0.toByte())
|
||||
@@ -55,15 +51,9 @@ class DatabaseInputOutputUtilsTest : TestCase() {
|
||||
|
||||
private fun testReadWriteLong(value: Byte) {
|
||||
val orig = ByteArray(8)
|
||||
val dest = ByteArray(8)
|
||||
|
||||
setArray(orig, value, 0, 8)
|
||||
|
||||
val one = LEDataInputStream.readLong(orig, 0)
|
||||
LEDataOutputStream.writeLong(one, dest, 0)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
setArray(orig, value, 8)
|
||||
|
||||
assertArrayEquals(orig, longTo8Bytes(bytes64ToLong(orig)))
|
||||
}
|
||||
|
||||
fun testReadWriteIntZero() {
|
||||
@@ -80,24 +70,22 @@ class DatabaseInputOutputUtilsTest : TestCase() {
|
||||
|
||||
private fun testReadWriteInt(value: Byte) {
|
||||
val orig = ByteArray(4)
|
||||
val dest = ByteArray(4)
|
||||
|
||||
for (i in 0..3) {
|
||||
orig[i] = 0
|
||||
}
|
||||
|
||||
setArray(orig, value, 0, 4)
|
||||
setArray(orig, value, 4)
|
||||
|
||||
val one = LEDataInputStream.readInt(orig, 0)
|
||||
|
||||
LEDataOutputStream.writeInt(one, dest, 0)
|
||||
val one = bytes4ToInt(orig)
|
||||
val dest = intTo4Bytes(one)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
private fun setArray(buf: ByteArray, value: Byte, offset: Int, size: Int) {
|
||||
for (i in offset until offset + size) {
|
||||
private fun setArray(buf: ByteArray, value: Byte, size: Int) {
|
||||
for (i in 0 until size) {
|
||||
buf[i] = value
|
||||
}
|
||||
}
|
||||
@@ -108,11 +96,10 @@ class DatabaseInputOutputUtilsTest : TestCase() {
|
||||
orig[0] = 0
|
||||
orig[1] = 1
|
||||
|
||||
val one = LEDataInputStream.readUShort(orig, 0)
|
||||
val dest = LEDataOutputStream.writeUShortBuf(one)
|
||||
val one = bytes2ToUShort(orig)
|
||||
val dest = uShortTo2Bytes(one)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
fun testReadWriteShortMin() {
|
||||
@@ -125,15 +112,12 @@ class DatabaseInputOutputUtilsTest : TestCase() {
|
||||
|
||||
private fun testReadWriteShort(value: Byte) {
|
||||
val orig = ByteArray(2)
|
||||
val dest = ByteArray(2)
|
||||
setArray(orig, value, 2)
|
||||
|
||||
setArray(orig, value, 0, 2)
|
||||
|
||||
val one = LEDataInputStream.readUShort(orig, 0)
|
||||
LEDataOutputStream.writeUShort(one, dest, 0)
|
||||
val one = bytes2ToUShort(orig)
|
||||
val dest = uShortTo2Bytes(one)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
fun testReadWriteByteZero() {
|
||||
@@ -149,16 +133,8 @@ class DatabaseInputOutputUtilsTest : TestCase() {
|
||||
}
|
||||
|
||||
private fun testReadWriteByte(value: Byte) {
|
||||
val orig = ByteArray(1)
|
||||
val dest = ByteArray(1)
|
||||
|
||||
setArray(orig, value, 0, 1)
|
||||
|
||||
val one = DatabaseInputOutputUtils.readUByte(orig, 0)
|
||||
DatabaseInputOutputUtils.writeUByte(one, dest, 0)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
val dest: Byte = uIntToByte(byteToUInt(value))
|
||||
assert(value == dest)
|
||||
}
|
||||
|
||||
fun testDate() {
|
||||
@@ -168,27 +144,33 @@ class DatabaseInputOutputUtilsTest : TestCase() {
|
||||
expected.set(2008, 1, 2, 3, 4, 5)
|
||||
|
||||
val actual = Calendar.getInstance()
|
||||
DatabaseInputOutputUtils.writeCDate(expected.time, cal)?.let { buf ->
|
||||
actual.time = DatabaseInputOutputUtils.readCDate(buf, 0, cal).date
|
||||
dateTo5Bytes(expected.time, cal)?.let { buf ->
|
||||
actual.time = bytes5ToDate(buf, cal).date
|
||||
}
|
||||
|
||||
val jDate = DateInstant(System.currentTimeMillis())
|
||||
val intermediate = DateInstant(jDate)
|
||||
val cDate = bytes5ToDate(dateTo5Bytes(intermediate.date)!!)
|
||||
|
||||
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
||||
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
||||
assertEquals("Day mismatch: ", 2, actual.get(Calendar.DAY_OF_MONTH))
|
||||
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY))
|
||||
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE))
|
||||
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND))
|
||||
assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
||||
assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
|
||||
}
|
||||
|
||||
fun testUUID() {
|
||||
val bUUID = ByteArray(16)
|
||||
Random().nextBytes(bUUID)
|
||||
|
||||
val uuid = DatabaseInputOutputUtils.bytesToUuid(bUUID)
|
||||
val eUUID = DatabaseInputOutputUtils.uuidToBytes(uuid)
|
||||
val uuid = bytes16ToUuid(bUUID)
|
||||
val eUUID = uuidTo16Bytes(uuid)
|
||||
|
||||
val lUUID = LEDataInputStream.readUuid(bUUID, 0)
|
||||
val leUUID = DatabaseInputOutputUtils.uuidToBytes(lUUID)
|
||||
val lUUID = bytes16ToUuid(bUUID)
|
||||
val leUUID = uuidTo16Bytes(lUUID)
|
||||
|
||||
assertArrayEquals("UUID match failed", bUUID, eUUID)
|
||||
assertArrayEquals("UUID match failed", bUUID, leUUID)
|
||||
@@ -202,8 +184,8 @@ class DatabaseInputOutputUtilsTest : TestCase() {
|
||||
}
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
val leos = LEDataOutputStream(bos)
|
||||
leos.writeLong(DatabaseInputOutputUtils.ULONG_MAX_VALUE)
|
||||
val leos = LittleEndianDataOutputStream(bos)
|
||||
leos.writeLong(ULONG_MAX_VALUE)
|
||||
leos.close()
|
||||
|
||||
val uLongMax = bos.toByteArray()
|
||||
@@ -39,9 +39,8 @@ import junit.framework.TestCase
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.stream.BetterCipherInputStream
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
|
||||
|
||||
class CipherTest : TestCase() {
|
||||
private val rand = Random()
|
||||
@@ -93,7 +92,7 @@ class CipherTest : TestCase() {
|
||||
|
||||
val bis = ByteArrayInputStream(secrettext)
|
||||
val cis = BetterCipherInputStream(bis, decrypt)
|
||||
val lis = LEDataInputStream(cis)
|
||||
val lis = LittleEndianDataInputStream(cis)
|
||||
|
||||
val decrypttext = lis.readBytes(MESSAGE_LENGTH)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.kunzisoft.keepass"
|
||||
android:installLocation="auto">
|
||||
<supports-screens
|
||||
@@ -7,10 +8,15 @@
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="true"
|
||||
android:anyDensity="true" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission
|
||||
android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission
|
||||
android:maxSdkVersion="18"
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
@@ -21,7 +27,9 @@
|
||||
android:fullBackupContent="@xml/backup"
|
||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/KeepassDXStyle.Night">
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/KeepassDXStyle.Night"
|
||||
tools:targetApi="n">
|
||||
<!-- TODO backup API Key -->
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
@@ -125,7 +133,7 @@
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/menu_about" />
|
||||
android:label="@string/about" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
||||
android:configChanges="keyboardHidden" />
|
||||
@@ -150,6 +158,10 @@
|
||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".notifications.AttachmentFileNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
||||
android:enabled="true"
|
||||
|
||||
@@ -21,15 +21,15 @@ package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.widget.TextView
|
||||
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.text.HtmlCompat
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class AboutActivity : StylishActivity() {
|
||||
@@ -40,7 +40,7 @@ class AboutActivity : StylishActivity() {
|
||||
setContentView(R.layout.activity_about)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.title = getString(R.string.menu_about)
|
||||
toolbar.title = getString(R.string.about)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
@@ -64,9 +64,17 @@ class AboutActivity : StylishActivity() {
|
||||
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
|
||||
buildTextView.text = build
|
||||
|
||||
findViewById<TextView>(R.id.activity_about_licence_text).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
|
||||
val disclaimerText = findViewById<TextView>(R.id.disclaimer)
|
||||
disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year)
|
||||
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
@@ -33,28 +34,40 @@ import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
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_RESTORE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
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.showActionError
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class EntryActivity : LockingHideActivity() {
|
||||
class EntryActivity : LockingActivity() {
|
||||
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
@@ -65,10 +78,16 @@ class EntryActivity : LockingHideActivity() {
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
private var mEntry: Entry? = null
|
||||
|
||||
private var mIsHistory: Boolean = false
|
||||
private var mEntryLastVersion: Entry? = null
|
||||
private var mEntryHistoryPosition: Int = -1
|
||||
|
||||
private var mShowPassword: Boolean = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var firstLaunchOfActivity: Boolean = false
|
||||
|
||||
@@ -98,6 +117,7 @@ class EntryActivity : LockingHideActivity() {
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Get views
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
titleIconView = findViewById(R.id.entry_icon)
|
||||
historyView = findViewById(R.id.history_container)
|
||||
@@ -108,6 +128,21 @@ class EntryActivity : LockingHideActivity() {
|
||||
// Init the clipboard helper
|
||||
clipboardHelper = ClipboardHelper(this)
|
||||
firstLaunchOfActivity = true
|
||||
|
||||
// Init attachment service binder manager
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
|
||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||
// Close the current activity after an history action
|
||||
if (result.isSuccess)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionError(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -117,11 +152,13 @@ class EntryActivity : LockingHideActivity() {
|
||||
try {
|
||||
val keyEntry: NodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
|
||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||
mEntryLastVersion = mEntry
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
|
||||
mEntryHistoryPosition = historyPosition
|
||||
if (historyPosition >= 0) {
|
||||
mIsHistory = true
|
||||
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
||||
@@ -155,9 +192,24 @@ class EntryActivity : LockingHideActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
mAttachmentFileBinderManager?.apply {
|
||||
registerProgressTask()
|
||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
|
||||
entryContentsView?.updateAttachmentDownloadProgress(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
firstLaunchOfActivity = false
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mAttachmentFileBinderManager?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun fillEntryDataInContentsView(entry: Entry) {
|
||||
|
||||
val database = Database.getInstance()
|
||||
@@ -265,6 +317,27 @@ class EntryActivity : LockingHideActivity() {
|
||||
}
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
|
||||
// Manage attachments
|
||||
val attachments = entry.getAttachments()
|
||||
val showAttachmentsView = attachments.isNotEmpty()
|
||||
entryContentsView?.showAttachments(showAttachmentsView)
|
||||
if (showAttachmentsView) {
|
||||
entryContentsView?.assignAttachments(attachments)
|
||||
entryContentsView?.onAttachmentClick { attachmentItem, _ ->
|
||||
when (attachmentItem.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
|
||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// TODO Stop download
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entryContentsView?.refreshAttachments()
|
||||
|
||||
// Assign dates
|
||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||
entryContentsView?.assignModificationDate(entry.lastModificationTime)
|
||||
@@ -276,9 +349,6 @@ class EntryActivity : LockingHideActivity() {
|
||||
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
||||
}
|
||||
|
||||
// Assign special data
|
||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||
|
||||
// Manage history
|
||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
||||
if (mIsHistory) {
|
||||
@@ -287,23 +357,26 @@ class EntryActivity : LockingHideActivity() {
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
val entryHistory = entry.getHistory()
|
||||
// isMainEntry = not an history
|
||||
// TODO isMainEntry = not an history
|
||||
val showHistoryView = entryHistory.isNotEmpty()
|
||||
entryContentsView?.showHistory(showHistoryView)
|
||||
if (showHistoryView) {
|
||||
entryContentsView?.assignHistory(entryHistory)
|
||||
entryContentsView?.onHistoryClick { historyItem, position ->
|
||||
launch(this, historyItem, true, position)
|
||||
launch(this, historyItem, mReadOnly, position)
|
||||
}
|
||||
}
|
||||
entryContentsView?.refreshHistory()
|
||||
|
||||
// Assign special data
|
||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||
|
||||
database.stopManageEntry(entry)
|
||||
|
||||
entryContentsView?.refreshHistory()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
||||
// Not directly get the entry from intent data but from database
|
||||
@@ -311,6 +384,15 @@ class EntryActivity : LockingHideActivity() {
|
||||
fillEntryDataInContentsView(it)
|
||||
}
|
||||
}
|
||||
|
||||
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||
if (createdFileUri != null) {
|
||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||
mAttachmentFileBinderManager
|
||||
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
|
||||
@@ -330,7 +412,10 @@ class EntryActivity : LockingHideActivity() {
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
if (mReadOnly) {
|
||||
if (mIsHistory && !mReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
}
|
||||
if (mIsHistory || mReadOnly) {
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
@@ -423,6 +508,22 @@ class EntryActivity : LockingHideActivity() {
|
||||
UriUtil.gotoUrl(this, url)
|
||||
return true
|
||||
}
|
||||
R.id.menu_restore_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
}
|
||||
}
|
||||
R.id.menu_delete_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mProgressDialogThread?.startDatabaseDeleteEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
}
|
||||
}
|
||||
R.id.menu_lock -> {
|
||||
lockAndExit()
|
||||
return true
|
||||
|
||||
@@ -28,12 +28,16 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
@@ -47,9 +51,10 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
import com.kunzisoft.keepass.view.showActionError
|
||||
import java.util.*
|
||||
|
||||
class EntryEditActivity : LockingHideActivity(),
|
||||
class EntryEditActivity : LockingActivity(),
|
||||
IconPickerDialogFragment.IconPickerListener,
|
||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||
SetOTPDialogFragment.CreateOtpListener {
|
||||
@@ -64,6 +69,7 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
private var mIsNew: Boolean = false
|
||||
|
||||
// Views
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var scrollView: ScrollView? = null
|
||||
private var entryEditContentsView: EntryEditContentsView? = null
|
||||
private var saveView: View? = null
|
||||
@@ -81,6 +87,8 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
|
||||
|
||||
scrollView = findViewById(R.id.entry_edit_scroll)
|
||||
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
|
||||
@@ -182,6 +190,7 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
finish()
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionError(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
@@ -53,8 +52,8 @@ import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||
import java.io.FileNotFoundException
|
||||
@@ -92,17 +91,14 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
|
||||
// Create button
|
||||
createButtonView = findViewById(R.id.create_database_button)
|
||||
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/x-keepass"
|
||||
}.resolveActivity(packageManager) == null) {
|
||||
// No Activity found that can handle this intent.
|
||||
createButtonView?.visibility = View.GONE
|
||||
}
|
||||
else{
|
||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||
// There is an activity which can handle this intent.
|
||||
createButtonView?.visibility = View.VISIBLE
|
||||
}
|
||||
else{
|
||||
// No Activity found that can handle this intent.
|
||||
createButtonView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
createButtonView?.setOnClickListener { createNewFile() }
|
||||
|
||||
@@ -182,18 +178,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun createNewFile() {
|
||||
try {
|
||||
startActivityForResult(Intent(
|
||||
Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/x-keepass"
|
||||
putExtra(Intent.EXTRA_TITLE, getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default))
|
||||
},
|
||||
CREATE_FILE_REQUEST_CODE)
|
||||
} catch (e: Exception) {
|
||||
BrowserDialogFragment().show(supportFragmentManager, "browserDialog")
|
||||
}
|
||||
createDocument(this, getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||
}
|
||||
|
||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||
@@ -289,13 +275,29 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
updateExternalStorageWarning()
|
||||
|
||||
// Construct adapter with listeners
|
||||
if (PreferencesUtil.showRecentFiles(this)) {
|
||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
||||
databaseFileHistoryList?.let {
|
||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
|
||||
updateFileListVisibility()
|
||||
databaseFileHistoryList?.let { historyList ->
|
||||
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(this@FileDatabaseSelectActivity)
|
||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(
|
||||
// Show only uri accessible
|
||||
historyList.filter {
|
||||
if (hideBrokenLocations) {
|
||||
UriUtil.parse(it.databaseUri)?.let { historyUri ->
|
||||
UriUtil.isUriAccessible(contentResolver, historyUri)
|
||||
} ?: false
|
||||
} else
|
||||
true
|
||||
})
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
}
|
||||
|
||||
// Register progress task
|
||||
mProgressDialogThread?.registerProgressTask()
|
||||
@@ -367,8 +369,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
}
|
||||
|
||||
// Retrieve the created URI from the file manager
|
||||
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||
mDatabaseFileUri = data?.data
|
||||
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
||||
mDatabaseFileUri = databaseFileCreatedUri
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
@@ -427,8 +429,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
private const val EXTRA_STAY = "EXTRA_STAY"
|
||||
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
||||
|
||||
private const val CREATE_FILE_REQUEST_CODE = 3853
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* No Standard Launch, pass by PasswordActivity
|
||||
|
||||
@@ -42,14 +42,19 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
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.*
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
@@ -71,6 +76,7 @@ import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.AddNodeButtonView
|
||||
import com.kunzisoft.keepass.view.ToolbarAction
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionError
|
||||
|
||||
class GroupActivity : LockingActivity(),
|
||||
GroupEditDialogFragment.EditGroupListener,
|
||||
@@ -96,6 +102,7 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
private var mListNodesFragment: ListNodesFragment? = null
|
||||
private var mCurrentGroupIsASearch: Boolean = false
|
||||
private var mRequestStartupSearch = true
|
||||
|
||||
// Nodes
|
||||
private var mRootGroup: Group? = null
|
||||
@@ -136,6 +143,8 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
// Retrieve elements after an orientation change
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(REQUEST_STARTUP_SEARCH_KEY))
|
||||
mRequestStartupSearch = savedInstanceState.getBoolean(REQUEST_STARTUP_SEARCH_KEY)
|
||||
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
|
||||
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
|
||||
}
|
||||
@@ -246,15 +255,7 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.isSuccess) {
|
||||
coordinatorLayout?.let { coordinatorLayout ->
|
||||
result.exception?.errorId?.let { errorId ->
|
||||
Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show()
|
||||
} ?: result.message?.let { message ->
|
||||
Snackbar.make(coordinatorLayout, message, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionError(result)
|
||||
|
||||
finishNodeAction()
|
||||
|
||||
@@ -334,6 +335,7 @@ class GroupActivity : LockingActivity(),
|
||||
mOldGroupToUpdate?.let {
|
||||
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
|
||||
}
|
||||
outState.putBoolean(REQUEST_STARTUP_SEARCH_KEY, mRequestStartupSearch)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
@@ -430,12 +432,8 @@ class GroupActivity : LockingActivity(),
|
||||
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
||||
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
||||
mCurrentGroup?.let {
|
||||
val isRoot = it == mRootGroup
|
||||
if (!it.allowAddEntryIfIsRoot())
|
||||
addEntryEnabled = !isRoot && addEntryEnabled
|
||||
if (isRoot) {
|
||||
showWarnings()
|
||||
}
|
||||
addEntryEnabled = it != mRootGroup && addEntryEnabled
|
||||
}
|
||||
enableAddGroup(addGroupEnabled)
|
||||
enableAddEntry(addEntryEnabled)
|
||||
@@ -448,7 +446,7 @@ class GroupActivity : LockingActivity(),
|
||||
private fun refreshNumberOfChildren() {
|
||||
numberChildrenView?.apply {
|
||||
if (PreferencesUtil.showNumberEntries(context)) {
|
||||
text = mCurrentGroup?.getChildEntries(true)?.size?.toString() ?: ""
|
||||
text = mCurrentGroup?.getNumberOfChildEntries(*Group.ChildFilter.getDefaults(context))?.toString() ?: ""
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
@@ -658,7 +656,8 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
// Menu for recycle bin
|
||||
if (mDatabase?.isRecycleBinEnabled == true
|
||||
if (!mReadOnly
|
||||
&& mDatabase?.isRecycleBinEnabled == true
|
||||
&& mDatabase?.recycleBin == mCurrentGroup) {
|
||||
inflater.inflate(R.menu.recycle_bin, menu)
|
||||
}
|
||||
@@ -688,6 +687,13 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
})
|
||||
}
|
||||
// Expand the search view if defined in settings
|
||||
if (mRequestStartupSearch
|
||||
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
|
||||
// To request search only one time
|
||||
mRequestStartupSearch = false
|
||||
it.expandActionView()
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
@@ -853,14 +859,6 @@ class GroupActivity : LockingActivity(),
|
||||
.iconPicked(bundle)
|
||||
}
|
||||
|
||||
private fun showWarnings() {
|
||||
if (Database.getInstance().isReadOnly) {
|
||||
if (PreferencesUtil.showReadOnlyWarning(this)) {
|
||||
ReadOnlyDialog().show(supportFragmentManager, "readOnlyDialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
|
||||
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
|
||||
}
|
||||
@@ -948,6 +946,7 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
private val TAG = GroupActivity::class.java.name
|
||||
|
||||
private const val REQUEST_STARTUP_SEARCH_KEY = "REQUEST_STARTUP_SEARCH_KEY"
|
||||
private const val GROUP_ID_KEY = "GROUP_ID_KEY"
|
||||
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
|
||||
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
@@ -35,7 +54,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
private var nodeClickListener: NodeClickListener? = null
|
||||
private var onScrollListener: OnScrollListener? = null
|
||||
|
||||
private var listView: RecyclerView? = null
|
||||
private var mNodesRecyclerView: RecyclerView? = null
|
||||
var mainGroup: Group? = null
|
||||
private set
|
||||
private var mAdapter: NodeAdapter? = null
|
||||
@@ -150,11 +169,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
// To apply theme
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_list_nodes, container, false)
|
||||
listView = rootView.findViewById(R.id.nodes_list)
|
||||
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
|
||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
||||
|
||||
mNodesRecyclerView?.apply {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = mAdapter
|
||||
}
|
||||
|
||||
onScrollListener?.let { onScrollListener ->
|
||||
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
onScrollListener.onScrolled(dy)
|
||||
@@ -162,8 +187,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
})
|
||||
}
|
||||
|
||||
rebuildList()
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
@@ -175,14 +198,14 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
mAdapter?.notifyDataSetChanged()
|
||||
rebuildList()
|
||||
|
||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
||||
// To show the " no search entry found "
|
||||
listView?.visibility = View.GONE
|
||||
mNodesRecyclerView?.visibility = View.GONE
|
||||
notFoundView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
listView?.visibility = View.VISIBLE
|
||||
mNodesRecyclerView?.visibility = View.VISIBLE
|
||||
notFoundView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
@@ -192,14 +215,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
listView?.apply {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = mAdapter
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean,
|
||||
recycleBinBottom: Boolean) {
|
||||
// Toggle setting
|
||||
prefs?.edit()?.apply {
|
||||
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
|
||||
@@ -210,10 +231,9 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
|
||||
// Tell the adapter to refresh it's list
|
||||
mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore)
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
mAdapter?.notifyChangeSort(sortNodeEnum,
|
||||
SortNodeEnum.SortNodeParameters(ascending, groupsBefore, recycleBinBottom))
|
||||
rebuildList()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
@@ -359,9 +379,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
mAdapter?.addNode(newNode)
|
||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
//mAdapter.updateLastNodeRegister(newNode);
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
rebuildList()
|
||||
}
|
||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||
}
|
||||
|
||||
@@ -32,10 +32,7 @@ import android.preference.PreferenceManager
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
@@ -75,11 +72,10 @@ import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_password.*
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class PasswordActivity : StylishActivity() {
|
||||
open class PasswordActivity : StylishActivity() {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var containerView: View? = null
|
||||
private var filenameView: TextView? = null
|
||||
private var passwordView: EditText? = null
|
||||
@@ -89,17 +85,28 @@ class PasswordActivity : StylishActivity() {
|
||||
private var checkboxKeyFileView: CompoundButton? = null
|
||||
private var checkboxDefaultDatabaseView: CompoundButton? = null
|
||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||
private var infoContainerView: ViewGroup? = null
|
||||
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
private var mDatabaseKeyFileUri: Uri? = null
|
||||
|
||||
private var prefs: SharedPreferences? = null
|
||||
private var mSharedPreferences: SharedPreferences? = null
|
||||
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
|
||||
private var readOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
set(value) {
|
||||
infoContainerView?.visibility = if (value) {
|
||||
readOnly = true
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
private var mProgressDialogThread: ProgressDialogThread? = null
|
||||
|
||||
@@ -108,9 +115,9 @@ class PasswordActivity : StylishActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
setContentView(R.layout.activity_password)
|
||||
|
||||
@@ -121,7 +128,7 @@ class PasswordActivity : StylishActivity() {
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
containerView = findViewById(R.id.container)
|
||||
confirmButtonView = findViewById(R.id.pass_ok)
|
||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||
filenameView = findViewById(R.id.filename)
|
||||
passwordView = findViewById(R.id.password)
|
||||
keyFileView = findViewById(R.id.pass_keyfile)
|
||||
@@ -129,6 +136,7 @@ class PasswordActivity : StylishActivity() {
|
||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
||||
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||
|
||||
@@ -179,6 +187,7 @@ class PasswordActivity : StylishActivity() {
|
||||
removePassword()
|
||||
|
||||
if (result.isSuccess) {
|
||||
setEmptyViews()
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
var resultError = ""
|
||||
@@ -300,6 +309,8 @@ class PasswordActivity : StylishActivity() {
|
||||
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
|
||||
}
|
||||
|
||||
mForceReadOnly = UriUtil.isUriNotWritable(contentResolver, databaseUri)
|
||||
|
||||
// Post init uri with KeyFile if needed
|
||||
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
|
||||
// Retrieve KeyFile in a thread
|
||||
@@ -338,7 +349,7 @@ class PasswordActivity : StylishActivity() {
|
||||
newDefaultFileName = databaseFileUri ?: newDefaultFileName
|
||||
}
|
||||
|
||||
prefs?.edit()?.apply {
|
||||
mSharedPreferences?.edit()?.apply {
|
||||
newDefaultFileName?.let {
|
||||
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
|
||||
} ?: kotlin.run {
|
||||
@@ -353,7 +364,7 @@ class PasswordActivity : StylishActivity() {
|
||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||
|
||||
// Retrieve settings for default database
|
||||
val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "")
|
||||
val defaultFilename = mSharedPreferences?.getString(KEY_DEFAULT_DATABASE_PATH, "")
|
||||
if (databaseFileUri != null
|
||||
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
||||
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
|
||||
@@ -363,6 +374,8 @@ class PasswordActivity : StylishActivity() {
|
||||
// If Activity is launch with a password and want to open directly
|
||||
val intent = intent
|
||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||
// Consume the intent extra password
|
||||
intent.removeExtra(KEY_PASSWORD)
|
||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||
if (password != null) {
|
||||
populatePasswordTextView(password)
|
||||
@@ -545,14 +558,16 @@ class PasswordActivity : StylishActivity() {
|
||||
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||
}
|
||||
|
||||
// To fix multiple view education
|
||||
private var performedEductionInProgress = false
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater = menuInflater
|
||||
// Read menu
|
||||
inflater.inflate(R.menu.open_file, menu)
|
||||
|
||||
if (mForceReadOnly) {
|
||||
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||
} else {
|
||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
}
|
||||
|
||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||
|
||||
@@ -563,51 +578,65 @@ class PasswordActivity : StylishActivity() {
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
if (!performedEductionInProgress) {
|
||||
performedEductionInProgress = true
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
}
|
||||
launchEducation(menu)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// To fix multiple view education
|
||||
private var performedEductionInProgress = false
|
||||
private fun launchEducation(menu: Menu, onEducationFinished: (()-> Unit)? = null) {
|
||||
if (!performedEductionInProgress) {
|
||||
performedEductionInProgress = true
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu, onEducationFinished) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||
menu: Menu) {
|
||||
val educationContainerView = containerView
|
||||
val unlockEducationPerformed = educationContainerView != null
|
||||
menu: Menu,
|
||||
onEducationFinished: (()-> Unit)? = null) {
|
||||
val educationToolbar = toolbar
|
||||
val unlockEducationPerformed = educationToolbar != null
|
||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||
educationContainerView,
|
||||
educationToolbar,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
||||
})
|
||||
if (!unlockEducationPerformed) {
|
||||
val educationToolbar = toolbar
|
||||
val readOnlyEducationPerformed =
|
||||
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
||||
})
|
||||
|
||||
if (!readOnlyEducationPerformed) {
|
||||
|
||||
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
|
||||
// EducationPerformed
|
||||
val biometricEducationPerformed =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
|
||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!)
|
||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
||||
})
|
||||
|
||||
if (!biometricEducationPerformed) {
|
||||
onEducationFinished?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class ReadOnlyDialog : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity)
|
||||
|
||||
var warning = getString(R.string.read_only_warning)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
warning = warning + "\n\n" + getString(R.string.read_only_kitkat_warning)
|
||||
}
|
||||
builder.setMessage(warning)
|
||||
|
||||
builder.setPositiveButton(getString(android.R.string.ok)) { _, _ -> dismiss() }
|
||||
builder.setNegativeButton(getString(R.string.beta_dontask)) { _, _ ->
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val edit = prefs.edit()
|
||||
edit.putBoolean(getString(R.string.show_read_only_warning), false)
|
||||
edit.apply()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.annotation.SuppressLint
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.helpers
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.helpers
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.lock
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
|
||||
/**
|
||||
* Locking Hide Activity that sets FLAG_SECURE to prevent screenshots, and from
|
||||
* appearing in the recent app preview
|
||||
*/
|
||||
abstract class LockingHideActivity : LockingActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Several gingerbread devices have problems with FLAG_SECURE
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
/* (non-Javadoc) Workaround for HTC Linkify issues
|
||||
* @see android.app.Activity#startActivity(android.content.Intent)
|
||||
*/
|
||||
override fun startActivity(intent: Intent) {
|
||||
try {
|
||||
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
|
||||
intent.component = null
|
||||
}
|
||||
super.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
/* Catch the bad HTC implementation case */
|
||||
super.startActivity(Intent.createChooser(intent, null))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ object Stylish {
|
||||
|
||||
return when (themeString) {
|
||||
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
||||
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
||||
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_red) -> R.style.KeepassDXStyle_Red
|
||||
|
||||
@@ -19,20 +19,45 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
|
||||
/**
|
||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||
* appearing in the recent app preview
|
||||
*/
|
||||
abstract class StylishActivity : AppCompatActivity() {
|
||||
|
||||
@StyleRes
|
||||
private var themeId: Int = 0
|
||||
|
||||
/* (non-Javadoc) Workaround for HTC Linkify issues
|
||||
* @see android.app.Activity#startActivity(android.content.Intent)
|
||||
*/
|
||||
override fun startActivity(intent: Intent) {
|
||||
try {
|
||||
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
|
||||
intent.component = null
|
||||
}
|
||||
super.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
/* Catch the bad HTC implementation case */
|
||||
super.startActivity(Intent.createChooser(intent, null))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
this.themeId = Stylish.getThemeId(this)
|
||||
setTheme(themeId)
|
||||
|
||||
// Several gingerbread devices have problems with FLAG_SECURE
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.Formatter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
|
||||
class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<EntryAttachmentsAdapter.EntryBinariesViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var entryAttachmentsList: MutableList<EntryAttachment> = ArrayList()
|
||||
var onItemClickListener: ((item: EntryAttachment, position: Int)->Unit)? = null
|
||||
|
||||
private val mDatabase = Database.getInstance()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
||||
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
||||
val entryAttachment = entryAttachmentsList[position]
|
||||
|
||||
holder.binaryFileTitle.text = entryAttachment.name
|
||||
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
||||
entryAttachment.binaryAttachment.length())
|
||||
holder.binaryFileCompression.apply {
|
||||
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||
|| entryAttachment.binaryAttachment.isCompressed == true) {
|
||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
text = ""
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
holder.binaryFileProgress.apply {
|
||||
visibility = when (entryAttachment.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
||||
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||
}
|
||||
progress = entryAttachment.downloadProgression
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClickListener?.invoke(entryAttachment, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return entryAttachmentsList.size
|
||||
}
|
||||
|
||||
fun updateProgress(entryAttachment: EntryAttachment) {
|
||||
val indexEntryAttachment = entryAttachmentsList.indexOfLast { current -> current.name == entryAttachment.name }
|
||||
if (indexEntryAttachment != -1) {
|
||||
entryAttachmentsList[indexEntryAttachment] = entryAttachment
|
||||
notifyItemChanged(indexEntryAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
entryAttachmentsList.clear()
|
||||
}
|
||||
|
||||
inner class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.adapters
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.adapters
|
||||
|
||||
import android.content.Context
|
||||
@@ -15,7 +34,7 @@ import java.util.ArrayList
|
||||
class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.FieldViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var fields: MutableList<Field> = ArrayList()
|
||||
private var fields: MutableList<Field> = ArrayList()
|
||||
var onItemClickListener: OnItemClickListener? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
|
||||
@@ -33,6 +52,11 @@ class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.Field
|
||||
return fields.size
|
||||
}
|
||||
|
||||
fun setFields(fieldsToAdd: List<Field>) {
|
||||
fields.clear()
|
||||
fields.addAll(fieldsToAdd)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
fields.clear()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.util.TypedValue
|
||||
@@ -82,15 +84,26 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
// File path
|
||||
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
||||
|
||||
holder.filePreciseInfoContainer.visibility = if (fileDatabaseInfo.found()) {
|
||||
// Modification
|
||||
holder.fileModification.text = fileDatabaseInfo.getModificationString()
|
||||
// Size
|
||||
holder.fileSize.text = fileDatabaseInfo.getSizeString()
|
||||
if (fileDatabaseInfo.dataAccessible()) {
|
||||
holder.fileInformation.clearColorFilter()
|
||||
} else {
|
||||
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
||||
}
|
||||
|
||||
View.VISIBLE
|
||||
} else
|
||||
View.GONE
|
||||
// Modification
|
||||
if (fileDatabaseInfo.lastModificationAccessible()) {
|
||||
holder.fileModification.text = fileDatabaseInfo.getModificationString()
|
||||
holder.fileModification.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.fileModification.visibility = View.GONE
|
||||
}
|
||||
// Size
|
||||
if (fileDatabaseInfo.sizeAccessible()) {
|
||||
holder.fileSize.text = fileDatabaseInfo.getSizeString()
|
||||
holder.fileSize.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.fileSize.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Click on information
|
||||
val isExpanded = position == mExpandedPosition
|
||||
@@ -142,6 +155,10 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
return listDatabaseFiles.size
|
||||
}
|
||||
|
||||
fun clearDatabaseFileHistoryList() {
|
||||
listDatabaseFiles.clear()
|
||||
}
|
||||
|
||||
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
|
||||
listDatabaseFiles.clear()
|
||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||
@@ -178,7 +195,6 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
||||
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
||||
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
||||
var filePreciseInfoContainer: ViewGroup = itemView.findViewById(R.id.file_precise_info_container)
|
||||
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
||||
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
||||
}
|
||||
|
||||
@@ -21,51 +21,52 @@ package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
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.*
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.setTextSize
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
import java.util.*
|
||||
|
||||
class NodeAdapter
|
||||
/**
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
(private val context: Context)
|
||||
class NodeAdapter (private val context: Context)
|
||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||
|
||||
private var nodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||
private val nodeSortedListCallback: NodeSortedListCallback
|
||||
private val nodeSortedList: SortedList<Node>
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
|
||||
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||
private var prefTextSize: Float = 0F
|
||||
private var subtextSize: Float = 0F
|
||||
private var infoTextSize: Float = 0F
|
||||
private var numberChildrenTextSize: Float = 0F
|
||||
private var iconSize: Float = 0F
|
||||
private var listSort: SortNodeEnum = SortNodeEnum.DB
|
||||
private var ascendingSort: Boolean = true
|
||||
private var groupsBeforeSort: Boolean = true
|
||||
private var recycleBinBottomSort: Boolean = true
|
||||
private var prefSizeMultiplier: Float = 0F
|
||||
private var subtextDefaultDimension: Float = 0F
|
||||
private var infoTextDefaultDimension: Float = 0F
|
||||
private var numberChildrenTextDefaultDimension: Float = 0F
|
||||
private var iconDefaultDimension: Float = 0F
|
||||
|
||||
private var showUserNames: Boolean = true
|
||||
private var showNumberEntries: Boolean = true
|
||||
private var entryFilters = arrayOf<Group.ChildFilter>()
|
||||
|
||||
private var actionNodesList = LinkedList<Node>()
|
||||
private var nodeClickCallback: NodeClickCallback? = null
|
||||
@@ -83,23 +84,15 @@ class NodeAdapter
|
||||
get() = nodeSortedList.size() <= 0
|
||||
|
||||
init {
|
||||
this.infoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
|
||||
this.subtextDefaultDimension = context.resources.getDimension(R.dimen.list_small_size_default)
|
||||
this.numberChildrenTextDefaultDimension = context.resources.getDimension(R.dimen.list_tiny_size_default)
|
||||
this.iconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
|
||||
|
||||
assignPreferences()
|
||||
|
||||
this.nodeSortedList = SortedList(Node::class.java, object : SortedListAdapterCallback<Node>(this) {
|
||||
override fun compare(item1: Node, item2: Node): Int {
|
||||
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||
return oldItem.type == newItem.type
|
||||
&& oldItem.title == newItem.title
|
||||
&& oldItem.icon == newItem.icon
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
|
||||
return item1 == item2
|
||||
}
|
||||
})
|
||||
this.nodeSortedListCallback = NodeSortedListCallback()
|
||||
this.nodeSortedList = SortedList(Node::class.java, nodeSortedListCallback)
|
||||
|
||||
// Database
|
||||
this.mDatabase = Database.getInstance()
|
||||
@@ -114,20 +107,23 @@ class NodeAdapter
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
private fun assignPreferences() {
|
||||
this.prefTextSize = PreferencesUtil.getListTextSize(context)
|
||||
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
|
||||
this.subtextSize = context.resources.getDimension(R.dimen.list_small_size_default) * prefTextSize
|
||||
this.numberChildrenTextSize = context.resources.getDimension(R.dimen.list_tiny_size_default) * prefTextSize
|
||||
this.iconSize = context.resources.getDimension(R.dimen.list_icon_size_default) * prefTextSize
|
||||
fun assignPreferences() {
|
||||
this.prefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
||||
|
||||
notifyChangeSort(
|
||||
PreferencesUtil.getListSort(context),
|
||||
SortNodeEnum.SortNodeParameters(
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
)
|
||||
)
|
||||
|
||||
this.listSort = PreferencesUtil.getListSort(context)
|
||||
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
|
||||
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
|
||||
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
|
||||
this.entryFilters = Group.ChildFilter.getDefaults(context)
|
||||
|
||||
// Reinit textSize for all view type
|
||||
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
|
||||
}
|
||||
@@ -136,15 +132,25 @@ class NodeAdapter
|
||||
* Rebuild the list by clear and build children from the group
|
||||
*/
|
||||
fun rebuildList(group: Group) {
|
||||
this.nodeSortedList.clear()
|
||||
assignPreferences()
|
||||
try {
|
||||
this.nodeSortedList.addAll(group.getChildren())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't add node elements to the list", e)
|
||||
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
|
||||
nodeSortedList.replaceAll(group.getFilteredChildren(*entryFilters)
|
||||
)
|
||||
}
|
||||
|
||||
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
||||
override fun compare(item1: Node, item2: Node): Int {
|
||||
return nodeComparator!!.compare(item1, item2)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||
return oldItem.type == newItem.type
|
||||
&& oldItem.title == newItem.title
|
||||
&& oldItem.icon == newItem.icon
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
|
||||
return item1 == item2
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun contains(node: Node): Boolean {
|
||||
@@ -255,10 +261,9 @@ class NodeAdapter
|
||||
/**
|
||||
* Notify a change sort of the list
|
||||
*/
|
||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) {
|
||||
this.listSort = sortNodeEnum
|
||||
this.ascendingSort = ascending
|
||||
this.groupsBeforeSort = groupsBefore
|
||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||
this.nodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
@@ -285,19 +290,57 @@ class NodeAdapter
|
||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||
// Relative size of the icon
|
||||
layoutParams?.apply {
|
||||
height = iconSize.toInt()
|
||||
width = iconSize.toInt()
|
||||
height = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
||||
width = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
// Assign text
|
||||
holder.text.apply {
|
||||
text = subNode.title
|
||||
setTextSize(textSizeUnit, infoTextSize)
|
||||
paintFlags = if (subNode.isCurrentlyExpires)
|
||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
else
|
||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
|
||||
setTextSize(textSizeUnit, infoTextDefaultDimension, prefSizeMultiplier)
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
}
|
||||
// Add subText with username
|
||||
holder.subText.apply {
|
||||
text = ""
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
visibility = View.GONE
|
||||
}
|
||||
|
||||
// Specific elements for entry
|
||||
if (subNode.type == Type.ENTRY) {
|
||||
val entry = subNode as Entry
|
||||
mDatabase.startManageEntry(entry)
|
||||
|
||||
holder.text.text = entry.getVisualTitle()
|
||||
holder.subText.apply {
|
||||
val username = entry.username
|
||||
if (showUserNames && username.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
text = username
|
||||
setTextSize(textSizeUnit, subtextDefaultDimension, prefSizeMultiplier)
|
||||
}
|
||||
}
|
||||
|
||||
mDatabase.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
// Add number of entries in groups
|
||||
if (subNode.type == Type.GROUP) {
|
||||
if (showNumberEntries) {
|
||||
holder.numberChildren?.apply {
|
||||
text = (subNode as Group)
|
||||
.getNumberOfChildEntries(*entryFilters)
|
||||
.toString()
|
||||
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
holder.numberChildren?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
// Assign click
|
||||
holder.container.setOnClickListener {
|
||||
nodeClickCallback?.onNodeClick(subNode)
|
||||
@@ -307,45 +350,6 @@ class NodeAdapter
|
||||
}
|
||||
|
||||
holder.container.isSelected = actionNodesList.contains(subNode)
|
||||
|
||||
// Add subText with username
|
||||
holder.subText.apply {
|
||||
text = ""
|
||||
paintFlags = if (subNode.isCurrentlyExpires)
|
||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
else
|
||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
|
||||
visibility = View.GONE
|
||||
if (subNode.type == Type.ENTRY) {
|
||||
val entry = subNode as Entry
|
||||
|
||||
mDatabase.startManageEntry(entry)
|
||||
|
||||
holder.text.text = entry.getVisualTitle()
|
||||
|
||||
val username = entry.username
|
||||
if (showUserNames && username.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
text = username
|
||||
setTextSize(textSizeUnit, subtextSize)
|
||||
}
|
||||
|
||||
mDatabase.stopManageEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Add number of entries in groups
|
||||
if (subNode.type == Type.GROUP) {
|
||||
if (showNumberEntries) {
|
||||
holder.numberChildren?.apply {
|
||||
text = (subNode as Group).getChildEntries(true).size.toString()
|
||||
setTextSize(textSizeUnit, numberChildrenTextSize)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
holder.numberChildren?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
|
||||
@@ -28,15 +28,14 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursor
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
|
||||
class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||
class SearchEntryCursorAdapter(private val context: Context,
|
||||
private val database: Database)
|
||||
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||
|
||||
private val cursorInflater: LayoutInflater = context.getSystemService(
|
||||
@@ -71,34 +70,31 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||
|
||||
override fun bindView(view: View, context: Context, cursor: Cursor) {
|
||||
|
||||
// Retrieve elements from cursor
|
||||
val uuid = UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))
|
||||
val iconFactory = database.iconFactory
|
||||
var icon: IconImage = iconFactory.getIcon(
|
||||
UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
|
||||
if (icon.isUnknown) {
|
||||
icon = iconFactory.getIcon(cursor.getInt(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_STANDARD)))
|
||||
if (icon.isUnknown)
|
||||
icon = iconFactory.keyIcon
|
||||
}
|
||||
val title = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_TITLE))
|
||||
val username = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_USERNAME))
|
||||
val url = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_URL))
|
||||
|
||||
database.getEntryFrom(cursor)?.let { currentEntry ->
|
||||
val viewHolder = view.tag as ViewHolder
|
||||
|
||||
// Assign image
|
||||
viewHolder.imageViewIcon?.assignDatabaseIcon(database.drawFactory, icon, iconColor)
|
||||
viewHolder.imageViewIcon?.assignDatabaseIcon(
|
||||
database.drawFactory,
|
||||
currentEntry.icon,
|
||||
iconColor)
|
||||
|
||||
// Assign title
|
||||
val showTitle = Entry.getVisualTitle(false, title, username, url, uuid.toString())
|
||||
viewHolder.textViewTitle?.text = showTitle
|
||||
if (displayUsername && username.isNotEmpty()) {
|
||||
viewHolder.textViewSubTitle?.text = String.format("(%s)", username)
|
||||
viewHolder.textViewTitle?.apply {
|
||||
text = currentEntry.getVisualTitle()
|
||||
strikeOut(currentEntry.isCurrentlyExpires)
|
||||
}
|
||||
|
||||
// Assign subtitle
|
||||
viewHolder.textViewSubTitle?.apply {
|
||||
val entryUsername = currentEntry.username
|
||||
text = if (displayUsername && entryUsername.isNotEmpty()) {
|
||||
String.format("(%s)", entryUsername)
|
||||
} else {
|
||||
viewHolder.textViewSubTitle?.text = ""
|
||||
""
|
||||
}
|
||||
strikeOut(currentEntry.isCurrentlyExpires)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +105,7 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||
}
|
||||
|
||||
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
||||
return database.searchEntries(constraint.toString())
|
||||
return database.searchEntries(context, constraint.toString())
|
||||
}
|
||||
|
||||
fun getEntryFromPosition(position: Int): Entry? {
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.app.database
|
||||
|
||||
import android.os.AsyncTask
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.app.database
|
||||
|
||||
import androidx.room.Database
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.app.database
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.app.database
|
||||
|
||||
import androidx.room.*
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.app.database
|
||||
|
||||
import android.os.Parcel
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.app.database
|
||||
|
||||
import androidx.room.*
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.app.database
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.biometric
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
@@ -19,15 +19,13 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto
|
||||
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
|
||||
import com.kunzisoft.keepass.stream.longTo8Bytes
|
||||
import java.io.IOException
|
||||
import java.security.DigestOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.Arrays
|
||||
|
||||
import java.util.*
|
||||
import javax.crypto.Mac
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -60,7 +58,7 @@ object CryptoUtil {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
val pbR = LEDataOutputStream.writeLongBuf(r)
|
||||
val pbR = longTo8Bytes(r)
|
||||
val part = hmac.doFinal(pbR)
|
||||
|
||||
val copy = min(cbOut - pos, part.size)
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.crypto.engine
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
@@ -47,7 +47,22 @@ class AesEngine : CipherEngine() {
|
||||
|
||||
companion object {
|
||||
|
||||
val CIPHER_UUID: UUID = DatabaseInputOutputUtils.bytesToUuid(
|
||||
byteArrayOf(0x31.toByte(), 0xC1.toByte(), 0xF2.toByte(), 0xE6.toByte(), 0xBF.toByte(), 0x71.toByte(), 0x43.toByte(), 0x50.toByte(), 0xBE.toByte(), 0x58.toByte(), 0x05.toByte(), 0x21.toByte(), 0x6A.toByte(), 0xFC.toByte(), 0x5A.toByte(), 0xFF.toByte()))
|
||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
||||
byteArrayOf(0x31.toByte(),
|
||||
0xC1.toByte(),
|
||||
0xF2.toByte(),
|
||||
0xE6.toByte(),
|
||||
0xBF.toByte(),
|
||||
0x71.toByte(),
|
||||
0x43.toByte(),
|
||||
0x50.toByte(),
|
||||
0xBE.toByte(),
|
||||
0x58.toByte(),
|
||||
0x05.toByte(),
|
||||
0x21.toByte(),
|
||||
0x6A.toByte(),
|
||||
0xFC.toByte(),
|
||||
0x5A.toByte(),
|
||||
0xFF.toByte()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
package com.kunzisoft.keepass.crypto.engine
|
||||
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
@@ -50,7 +50,22 @@ class ChaCha20Engine : CipherEngine() {
|
||||
|
||||
companion object {
|
||||
|
||||
val CIPHER_UUID: UUID = DatabaseInputOutputUtils.bytesToUuid(
|
||||
byteArrayOf(0xD6.toByte(), 0x03.toByte(), 0x8A.toByte(), 0x2B.toByte(), 0x8B.toByte(), 0x6F.toByte(), 0x4C.toByte(), 0xB5.toByte(), 0xA5.toByte(), 0x24.toByte(), 0x33.toByte(), 0x9A.toByte(), 0x31.toByte(), 0xDB.toByte(), 0xB5.toByte(), 0x9A.toByte()))
|
||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
||||
byteArrayOf(0xD6.toByte(),
|
||||
0x03.toByte(),
|
||||
0x8A.toByte(),
|
||||
0x2B.toByte(),
|
||||
0x8B.toByte(),
|
||||
0x6F.toByte(),
|
||||
0x4C.toByte(),
|
||||
0xB5.toByte(),
|
||||
0xA5.toByte(),
|
||||
0x24.toByte(),
|
||||
0x33.toByte(),
|
||||
0x9A.toByte(),
|
||||
0x31.toByte(),
|
||||
0xDB.toByte(),
|
||||
0xB5.toByte(),
|
||||
0x9A.toByte()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,11 @@ package com.kunzisoft.keepass.crypto.engine
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
|
||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.UUID
|
||||
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
@@ -53,7 +51,22 @@ class TwofishEngine : CipherEngine() {
|
||||
|
||||
companion object {
|
||||
|
||||
val CIPHER_UUID: UUID = DatabaseInputOutputUtils.bytesToUuid(
|
||||
byteArrayOf(0xAD.toByte(), 0x68.toByte(), 0xF2.toByte(), 0x9F.toByte(), 0x57.toByte(), 0x6F.toByte(), 0x4B.toByte(), 0xB9.toByte(), 0xA3.toByte(), 0x6A.toByte(), 0xD4.toByte(), 0x7A.toByte(), 0xF9.toByte(), 0x65.toByte(), 0x34.toByte(), 0x6C.toByte()))
|
||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
||||
byteArrayOf(0xAD.toByte(),
|
||||
0x68.toByte(),
|
||||
0xF2.toByte(),
|
||||
0x9F.toByte(),
|
||||
0x57.toByte(),
|
||||
0x6F.toByte(),
|
||||
0x4B.toByte(),
|
||||
0xB9.toByte(),
|
||||
0xA3.toByte(),
|
||||
0x6A.toByte(),
|
||||
0xD4.toByte(),
|
||||
0x7A.toByte(),
|
||||
0xF9.toByte(),
|
||||
0x65.toByte(),
|
||||
0x34.toByte(),
|
||||
0x6C.toByte()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import android.content.res.Resources
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.crypto.CryptoUtil
|
||||
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||
import java.io.IOException
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
@@ -88,7 +88,7 @@ class AesKdf internal constructor() : KdfEngine() {
|
||||
|
||||
private const val DEFAULT_ROUNDS = 6000
|
||||
|
||||
val CIPHER_UUID: UUID = DatabaseInputOutputUtils.bytesToUuid(
|
||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
||||
byteArrayOf(0xC9.toByte(),
|
||||
0xD9.toByte(),
|
||||
0xF3.toByte(),
|
||||
|
||||
@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.crypto.keyDerivation
|
||||
|
||||
import android.content.res.Resources
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||
import java.io.IOException
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
@@ -126,7 +126,7 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
||||
|
||||
companion object {
|
||||
|
||||
val CIPHER_UUID: UUID = DatabaseInputOutputUtils.bytesToUuid(
|
||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
||||
byteArrayOf(0xEF.toByte(),
|
||||
0x63.toByte(),
|
||||
0x6D.toByte(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
@@ -19,20 +19,20 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
||||
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||
import com.kunzisoft.keepass.stream.uuidTo16Bytes
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
|
||||
class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
|
||||
|
||||
fun setParamUUID() {
|
||||
setByteArray(PARAM_UUID, DatabaseInputOutputUtils.uuidToBytes(uuid))
|
||||
setByteArray(PARAM_UUID, uuidTo16Bytes(uuid))
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -42,11 +42,11 @@ class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
|
||||
@Throws(IOException::class)
|
||||
fun deserialize(data: ByteArray): KdfParameters? {
|
||||
val bis = ByteArrayInputStream(data)
|
||||
val lis = LEDataInputStream(bis)
|
||||
val lis = LittleEndianDataInputStream(bis)
|
||||
|
||||
val d = deserialize(lis) ?: return null
|
||||
|
||||
val uuid = DatabaseInputOutputUtils.bytesToUuid(d.getByteArray(PARAM_UUID))
|
||||
val uuid = bytes16ToUuid(d.getByteArray(PARAM_UUID))
|
||||
|
||||
val kdfP = KdfParameters(uuid)
|
||||
kdfP.copyTo(d)
|
||||
@@ -56,7 +56,7 @@ class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
|
||||
@Throws(IOException::class)
|
||||
fun serialize(kdf: KdfParameters): ByteArray {
|
||||
val bos = ByteArrayOutputStream()
|
||||
val los = LEDataOutputStream(bos)
|
||||
val los = LittleEndianDataOutputStream(bos)
|
||||
|
||||
serialize(kdf, los)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
class CreateDatabaseRunnable(context: Context,
|
||||
private val mDatabase: Database,
|
||||
@@ -57,8 +58,11 @@ class CreateDatabaseRunnable(context: Context,
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Add database to recent files
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||
.addOrUpdateDatabaseUri(mDatabaseUri, mKeyFile)
|
||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFile else null)
|
||||
}
|
||||
} else {
|
||||
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
||||
}
|
||||
|
||||
@@ -74,14 +74,10 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
override fun onFinishRun() {
|
||||
if (result.isSuccess) {
|
||||
// Save keyFile in app database
|
||||
val rememberKeyFile = PreferencesUtil.rememberKeyFiles(context)
|
||||
if (rememberKeyFile) {
|
||||
var keyUri = mKey
|
||||
if (!rememberKeyFile) {
|
||||
keyUri = null
|
||||
}
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
FileDatabaseHistoryAction.getInstance(context)
|
||||
.addOrUpdateDatabaseUri(mUri, keyUri)
|
||||
.addOrUpdateDatabaseUri(mUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null)
|
||||
}
|
||||
|
||||
// Register the biometric
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.action
|
||||
|
||||
import android.content.*
|
||||
@@ -22,9 +41,11 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
|
||||
@@ -178,7 +199,11 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
||||
|
||||
unBindService()
|
||||
|
||||
try {
|
||||
activity.unregisterReceiver(databaseTaskBroadcastReceiver)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// If receiver not register, do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -348,6 +373,34 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
||||
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
|
||||
}
|
||||
|
||||
/*
|
||||
-----------------
|
||||
Entry History Settings
|
||||
-----------------
|
||||
*/
|
||||
|
||||
fun startDatabaseRestoreEntryHistory(mainEntry: Entry,
|
||||
entryHistoryPosition: Int,
|
||||
save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
|
||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||
}
|
||||
|
||||
fun startDatabaseDeleteEntryHistory(mainEntry: Entry,
|
||||
entryHistoryPosition: Int,
|
||||
save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
|
||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
||||
}
|
||||
|
||||
/*
|
||||
-----------------
|
||||
Main Settings
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020 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.action.history
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
|
||||
class DeleteEntryHistoryDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val mainEntry: Entry,
|
||||
private val entryHistoryPosition: Int,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
mainEntry.removeEntryFromHistory(entryHistoryPosition)
|
||||
} catch (e: Exception) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
super.onStartRun()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2020 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.action.history
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class RestoreEntryHistoryDatabaseRunnable (
|
||||
private val context: Context,
|
||||
private val database: Database,
|
||||
private val mainEntry: Entry,
|
||||
private val entryHistoryPosition: Int,
|
||||
private val saveDatabase: Boolean)
|
||||
: ActionRunnable() {
|
||||
|
||||
private var updateEntryRunnable: UpdateEntryRunnable? = null
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
val historyToRestore = Entry(mainEntry.getHistory()[entryHistoryPosition])
|
||||
// Copy history of main entry in the restore entry
|
||||
mainEntry.getHistory().forEach {
|
||||
historyToRestore.addEntryToHistory(it)
|
||||
}
|
||||
// Update the entry with the fresh formatted entry to restore
|
||||
updateEntryRunnable = UpdateEntryRunnable(context,
|
||||
database,
|
||||
mainEntry,
|
||||
historyToRestore,
|
||||
saveDatabase,
|
||||
null)
|
||||
|
||||
updateEntryRunnable?.onStartRun()
|
||||
|
||||
} catch (e: Exception) {
|
||||
setError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
updateEntryRunnable?.onActionRun()
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
updateEntryRunnable?.onFinishRun()
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
/*
|
||||
* 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.database.cursor
|
||||
|
||||
import android.database.MatrixCursor
|
||||
import android.provider.BaseColumns
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import java.util.*
|
||||
|
||||
abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>> : MatrixCursor(arrayOf(
|
||||
_ID,
|
||||
@@ -17,7 +38,9 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
|
||||
COLUMN_INDEX_USERNAME,
|
||||
COLUMN_INDEX_PASSWORD,
|
||||
COLUMN_INDEX_URL,
|
||||
COLUMN_INDEX_NOTES
|
||||
COLUMN_INDEX_NOTES,
|
||||
COLUMN_INDEX_EXPIRY_TIME,
|
||||
COLUMN_INDEX_EXPIRES
|
||||
)) {
|
||||
|
||||
protected var entryId: Long = 0
|
||||
@@ -37,6 +60,9 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
|
||||
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))
|
||||
pwEntry.url = getString(getColumnIndex(COLUMN_INDEX_URL))
|
||||
pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES))
|
||||
pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME)))
|
||||
pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES))
|
||||
.toLowerCase(Locale.ENGLISH) != "false"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -51,5 +77,7 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
|
||||
const val COLUMN_INDEX_PASSWORD = "password"
|
||||
const val COLUMN_INDEX_URL = "URL"
|
||||
const val COLUMN_INDEX_NOTES = "notes"
|
||||
const val COLUMN_INDEX_EXPIRY_TIME = "expiry_time"
|
||||
const val COLUMN_INDEX_EXPIRES = "expires"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.cursor
|
||||
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
@@ -17,7 +36,9 @@ class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
|
||||
entry.username,
|
||||
entry.password,
|
||||
entry.url,
|
||||
entry.notes
|
||||
entry.notes,
|
||||
entry.expiryTime,
|
||||
entry.expires
|
||||
))
|
||||
entryId++
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.cursor
|
||||
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
@@ -21,7 +40,9 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
||||
entry.username,
|
||||
entry.password,
|
||||
entry.url,
|
||||
entry.notes
|
||||
entry.notes,
|
||||
entry.expiryTime,
|
||||
entry.expires
|
||||
))
|
||||
|
||||
for (element in entry.customFields.entries) {
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.cursor
|
||||
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.cursor
|
||||
|
||||
import android.database.MatrixCursor
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
@@ -36,7 +37,10 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
||||
@@ -45,7 +49,7 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
import com.kunzisoft.keepass.stream.readBytes4ToInt
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
@@ -324,47 +328,37 @@ class Database {
|
||||
isReadOnly = !file.canWrite()
|
||||
}
|
||||
|
||||
// Pass Uris as InputStreams
|
||||
val inputStream: InputStream?
|
||||
try {
|
||||
inputStream = UriUtil.getUriInputStream(contentResolver, uri)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw FileNotFoundDatabaseException()
|
||||
}
|
||||
|
||||
// Pass KeyFile Uri as InputStreams
|
||||
var databaseInputStream: InputStream? = null
|
||||
var keyFileInputStream: InputStream? = null
|
||||
keyfile?.let {
|
||||
try {
|
||||
// Get keyFile inputStream
|
||||
keyfile?.let {
|
||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw FileNotFoundDatabaseException()
|
||||
}
|
||||
}
|
||||
|
||||
// Load Data
|
||||
|
||||
val bufferedInputStream = BufferedInputStream(inputStream)
|
||||
if (!bufferedInputStream.markSupported()) {
|
||||
// Load Data, pass Uris as InputStreams
|
||||
databaseInputStream = BufferedInputStream(UriUtil.getUriInputStream(contentResolver, uri))
|
||||
if (!databaseInputStream.markSupported()) {
|
||||
throw IOException("Input stream does not support mark.")
|
||||
}
|
||||
|
||||
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
|
||||
bufferedInputStream.mark(10)
|
||||
databaseInputStream.mark(10)
|
||||
|
||||
// Get the file directory to save the attachments
|
||||
val sig1 = LEDataInputStream.readInt(bufferedInputStream)
|
||||
val sig2 = LEDataInputStream.readInt(bufferedInputStream)
|
||||
val sig1 = databaseInputStream.readBytes4ToInt()
|
||||
val sig2 = databaseInputStream.readBytes4ToInt()
|
||||
|
||||
// Return to the start
|
||||
bufferedInputStream.reset()
|
||||
databaseInputStream.reset()
|
||||
|
||||
when {
|
||||
// Header of database KDB
|
||||
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(DatabaseInputKDB()
|
||||
.openDatabase(bufferedInputStream,
|
||||
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(DatabaseInputKDB(
|
||||
cacheDirectory,
|
||||
fixDuplicateUUID)
|
||||
.openDatabase(databaseInputStream,
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater))
|
||||
@@ -373,7 +367,7 @@ class Database {
|
||||
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(DatabaseInputKDBX(
|
||||
cacheDirectory,
|
||||
fixDuplicateUUID)
|
||||
.openDatabase(bufferedInputStream,
|
||||
.openDatabase(databaseInputStream,
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater))
|
||||
@@ -384,6 +378,17 @@ class Database {
|
||||
|
||||
this.mSearchHelper = SearchHelper(omitBackup)
|
||||
loaded = true
|
||||
|
||||
} catch (e: LoadDatabaseException) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw FileNotFoundDatabaseException()
|
||||
} finally {
|
||||
keyFileInputStream?.close()
|
||||
databaseInputStream?.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun isGroupSearchable(group: Group, isOmitBackup: Boolean): Boolean {
|
||||
@@ -397,7 +402,7 @@ class Database {
|
||||
return mSearchHelper?.search(this, str, max)
|
||||
}
|
||||
|
||||
fun searchEntries(query: String): Cursor? {
|
||||
fun searchEntries(context: Context, query: String): Cursor? {
|
||||
|
||||
var cursorKDB: EntryCursorKDB? = null
|
||||
var cursorKDBX: EntryCursorKDBX? = null
|
||||
@@ -409,7 +414,8 @@ class Database {
|
||||
|
||||
val searchResult = search(query, SearchHelper.MAX_SEARCH_ENTRY)
|
||||
if (searchResult != null) {
|
||||
for (entry in searchResult.getChildEntries(true)) {
|
||||
// Search in hide entries but not meta-stream
|
||||
for (entry in searchResult.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
|
||||
entry.entryKDB?.let {
|
||||
cursorKDB?.addEntry(it)
|
||||
}
|
||||
@@ -424,25 +430,21 @@ class Database {
|
||||
|
||||
fun getEntryFrom(cursor: Cursor): Entry? {
|
||||
val iconFactory = mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
|
||||
val entry = createEntry()
|
||||
|
||||
// TODO invert field reference manager
|
||||
entry?.let { entryVersioned ->
|
||||
startManageEntry(entryVersioned)
|
||||
return createEntry()?.apply {
|
||||
startManageEntry(this)
|
||||
mDatabaseKDB?.let {
|
||||
entryVersioned.entryKDB?.let { entryKDB ->
|
||||
entryKDB?.let { entryKDB ->
|
||||
(cursor as EntryCursorKDB).populateEntry(entryKDB, iconFactory)
|
||||
}
|
||||
}
|
||||
mDatabaseKDBX?.let {
|
||||
entryVersioned.entryKDBX?.let { entryKDBX ->
|
||||
entryKDBX?.let { entryKDBX ->
|
||||
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, iconFactory)
|
||||
}
|
||||
}
|
||||
stopManageEntry(entryVersioned)
|
||||
stopManageEntry(this)
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.content.res.Resources
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class DateInstant : Parcelable {
|
||||
@@ -44,6 +45,10 @@ class DateInstant : Parcelable {
|
||||
jDate = Date(millis)
|
||||
}
|
||||
|
||||
constructor(string: String) {
|
||||
jDate = dateFormat.parse(string)
|
||||
}
|
||||
|
||||
constructor() {
|
||||
jDate = Date()
|
||||
}
|
||||
@@ -84,12 +89,13 @@ class DateInstant : Parcelable {
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return jDate.toString()
|
||||
return dateFormat.format(jDate)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val NEVER_EXPIRE = neverExpire
|
||||
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
||||
|
||||
private val neverExpire: DateInstant
|
||||
get() {
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
@@ -13,7 +32,9 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
@@ -139,6 +160,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
return contained ?: false
|
||||
}
|
||||
|
||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||
return entryKDB?.nodeIndexInParentForNaturalOrder()
|
||||
?: entryKDBX?.nodeIndexInParentForNaturalOrder()
|
||||
?: -1
|
||||
}
|
||||
|
||||
override var creationTime: DateInstant
|
||||
get() = entryKDB?.creationTime ?: entryKDBX?.creationTime ?: DateInstant()
|
||||
set(value) {
|
||||
@@ -209,12 +236,27 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
return title == PMS_TAN_ENTRY && username.isNotEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* Get the display title from an entry, <br></br>
|
||||
* [.startManageEntry] and [.stopManageEntry] must be called
|
||||
* before and after [.getVisualTitle]
|
||||
*/
|
||||
fun getVisualTitle(): String {
|
||||
return getVisualTitle(isTan(),
|
||||
title,
|
||||
username,
|
||||
url,
|
||||
nodeId.toString())
|
||||
return if (isTan()) {
|
||||
"$PMS_TAN_ENTRY $username"
|
||||
} else {
|
||||
if (title.isEmpty())
|
||||
if (url.isEmpty())
|
||||
if (username.isEmpty())
|
||||
nodeId.toString()
|
||||
else
|
||||
username
|
||||
else
|
||||
url
|
||||
else
|
||||
title
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -284,10 +326,29 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
entryKDBX?.stopToManageFieldReferences()
|
||||
}
|
||||
|
||||
fun getAttachments(): ArrayList<EntryAttachment> {
|
||||
val attachments = ArrayList<EntryAttachment>()
|
||||
|
||||
val binaryDescriptionKDB = entryKDB?.binaryDescription ?: ""
|
||||
val binaryKDB = entryKDB?.binaryData
|
||||
if (binaryKDB != null) {
|
||||
attachments.add(EntryAttachment(binaryDescriptionKDB, binaryKDB))
|
||||
}
|
||||
|
||||
val actionEach = object : (Map.Entry<String, BinaryAttachment>)->Unit {
|
||||
override fun invoke(mapEntry: Map.Entry<String, BinaryAttachment>) {
|
||||
attachments.add(EntryAttachment(mapEntry.key, mapEntry.value))
|
||||
}
|
||||
}
|
||||
entryKDBX?.binaries?.forEach(actionEach)
|
||||
|
||||
return attachments
|
||||
}
|
||||
|
||||
fun getHistory(): ArrayList<Entry> {
|
||||
val history = ArrayList<Entry>()
|
||||
val entryV4History = entryKDBX?.history ?: ArrayList()
|
||||
for (entryHistory in entryV4History) {
|
||||
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
|
||||
for (entryHistory in entryKDBXHistory) {
|
||||
history.add(Entry(entryHistory))
|
||||
}
|
||||
return history
|
||||
@@ -299,6 +360,10 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEntryFromHistory(position: Int) {
|
||||
entryKDBX?.removeEntryFromHistory(position)
|
||||
}
|
||||
|
||||
fun removeAllHistory() {
|
||||
entryKDBX?.removeAllHistory()
|
||||
}
|
||||
@@ -379,28 +444,5 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
}
|
||||
|
||||
const val PMS_TAN_ENTRY = "<TAN>"
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* Get the display title from an entry, <br></br>
|
||||
* [.startManageEntry] and [.stopManageEntry] must be called
|
||||
* before and after [.getVisualTitle]
|
||||
*/
|
||||
fun getVisualTitle(isTan: Boolean, title: String, userName: String, url: String, id: String): String {
|
||||
return if (isTan) {
|
||||
"$PMS_TAN_ENTRY $userName"
|
||||
} else {
|
||||
if (title.isEmpty())
|
||||
if (userName.isEmpty())
|
||||
if (url.isEmpty())
|
||||
id
|
||||
else
|
||||
url
|
||||
else
|
||||
userName
|
||||
else
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
/*
|
||||
* 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.database.element
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
@@ -8,6 +28,7 @@ import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||
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.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@@ -57,6 +78,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
||||
}
|
||||
|
||||
enum class ChildFilter {
|
||||
META_STREAM, EXPIRED;
|
||||
|
||||
companion object {
|
||||
fun getDefaults(context: Context): Array<ChildFilter> {
|
||||
return if (PreferencesUtil.showExpiredEntries(context)) {
|
||||
arrayOf(META_STREAM)
|
||||
} else {
|
||||
arrayOf(META_STREAM, EXPIRED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Group> {
|
||||
override fun createFromParcel(parcel: Parcel): Group {
|
||||
return Group(parcel)
|
||||
@@ -152,6 +187,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
return contained ?: false
|
||||
}
|
||||
|
||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||
return groupKDB?.nodeIndexInParentForNaturalOrder()
|
||||
?: groupKDBX?.nodeIndexInParentForNaturalOrder()
|
||||
?: -1
|
||||
}
|
||||
|
||||
override var creationTime: DateInstant
|
||||
get() = groupKDB?.creationTime ?: groupKDBX?.creationTime ?: DateInstant()
|
||||
set(value) {
|
||||
@@ -190,55 +231,58 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
override val isCurrentlyExpires: Boolean
|
||||
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
|
||||
|
||||
override fun getChildGroups(): MutableList<Group> {
|
||||
val children = ArrayList<Group>()
|
||||
|
||||
groupKDB?.getChildGroups()?.forEach {
|
||||
children.add(Group(it))
|
||||
}
|
||||
groupKDBX?.getChildGroups()?.forEach {
|
||||
children.add(Group(it))
|
||||
override fun getChildGroups(): List<Group> {
|
||||
return groupKDB?.getChildGroups()?.map {
|
||||
Group(it)
|
||||
} ?:
|
||||
groupKDBX?.getChildGroups()?.map {
|
||||
Group(it)
|
||||
} ?:
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
return children
|
||||
override fun getChildEntries(): List<Entry> {
|
||||
return groupKDB?.getChildEntries()?.map {
|
||||
Entry(it)
|
||||
} ?:
|
||||
groupKDBX?.getChildEntries()?.map {
|
||||
Entry(it)
|
||||
} ?:
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
override fun getChildEntries(): MutableList<Entry> {
|
||||
return getChildEntries(false)
|
||||
fun getFilteredChildEntries(vararg filter: ChildFilter): List<Entry> {
|
||||
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
|
||||
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)
|
||||
|
||||
return groupKDB?.getChildEntries()?.filter {
|
||||
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
|
||||
&& (!it.isCurrentlyExpires or showExpiredEntries)
|
||||
}?.map {
|
||||
Entry(it)
|
||||
} ?:
|
||||
groupKDBX?.getChildEntries()?.filter {
|
||||
!it.isCurrentlyExpires or showExpiredEntries
|
||||
}?.map {
|
||||
Entry(it)
|
||||
} ?:
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
fun getChildEntries(withoutMetaStream: Boolean): MutableList<Entry> {
|
||||
val children = ArrayList<Entry>()
|
||||
|
||||
groupKDB?.getChildEntries()?.forEach {
|
||||
val entryToAddAsChild = Entry(it)
|
||||
if (!withoutMetaStream || (withoutMetaStream && !entryToAddAsChild.isMetaStream))
|
||||
children.add(entryToAddAsChild)
|
||||
}
|
||||
groupKDBX?.getChildEntries()?.forEach {
|
||||
children.add(Entry(it))
|
||||
}
|
||||
|
||||
return children
|
||||
fun getNumberOfChildEntries(vararg filter: ChildFilter): Int {
|
||||
return getFilteredChildEntries(*filter).size
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter MetaStream entries and return children
|
||||
* Filter entries and return children
|
||||
* @return List of direct children (one level below) as NodeVersioned
|
||||
*/
|
||||
fun getChildren(withoutMetaStream: Boolean = true): List<Node> {
|
||||
val children = ArrayList<Node>()
|
||||
children.addAll(getChildGroups())
|
||||
|
||||
groupKDB?.let {
|
||||
children.addAll(getChildEntries(withoutMetaStream))
|
||||
}
|
||||
groupKDBX?.let {
|
||||
// No MetasStream in V4
|
||||
children.addAll(getChildEntries(withoutMetaStream))
|
||||
fun getChildren(): List<Node> {
|
||||
return getChildGroups() + getChildEntries()
|
||||
}
|
||||
|
||||
return children
|
||||
fun getFilteredChildren(vararg filter: ChildFilter): List<Node> {
|
||||
return getChildGroups() + getFilteredChildEntries(*filter)
|
||||
}
|
||||
|
||||
override fun addChildGroup(group: Group) {
|
||||
|
||||
@@ -20,149 +20,198 @@
|
||||
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import java.util.*
|
||||
|
||||
enum class SortNodeEnum {
|
||||
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
|
||||
|
||||
fun getNodeComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean): Comparator<Node> {
|
||||
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(sortNodeParameters: SortNodeParameters)
|
||||
: Comparator<NodeVersionedInterface<G>> {
|
||||
return when (this) {
|
||||
DB -> NodeNaturalComparator(ascending, groupsBefore, false) // Force false because natural order contains recycle bin
|
||||
TITLE -> NodeTitleComparator(ascending, groupsBefore, recycleBinBottom)
|
||||
USERNAME -> NodeUsernameComparator(ascending, groupsBefore, recycleBinBottom)
|
||||
CREATION_TIME -> NodeCreationComparator(ascending, groupsBefore, recycleBinBottom)
|
||||
LAST_MODIFY_TIME -> NodeLastModificationComparator(ascending, groupsBefore, recycleBinBottom)
|
||||
LAST_ACCESS_TIME -> NodeLastAccessComparator(ascending, groupsBefore, recycleBinBottom)
|
||||
DB -> NodeNaturalComparator(sortNodeParameters) // Force false because natural order contains recycle bin
|
||||
TITLE -> NodeTitleComparator(sortNodeParameters)
|
||||
USERNAME -> NodeUsernameComparator(sortNodeParameters)
|
||||
CREATION_TIME -> NodeCreationComparator(sortNodeParameters)
|
||||
LAST_MODIFY_TIME -> NodeLastModificationComparator(sortNodeParameters)
|
||||
LAST_ACCESS_TIME -> NodeLastAccessComparator(sortNodeParameters)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class NodeComparator(var ascending: Boolean, var groupsBefore: Boolean, var recycleBinBottom: Boolean) : Comparator<Node> {
|
||||
data class SortNodeParameters(var ascending: Boolean = true,
|
||||
var groupsBefore: Boolean = true,
|
||||
var recycleBinBottom: Boolean = true)
|
||||
|
||||
abstract fun compareBySpecificOrder(object1: Node, object2: Node): Int
|
||||
abstract class NodeComparator
|
||||
<
|
||||
G: GroupVersionedInterface<*, *>,
|
||||
T: NodeVersionedInterface<G>
|
||||
>(var sortNodeParameters: SortNodeParameters)
|
||||
: Comparator<T> {
|
||||
|
||||
private fun specificOrderOrHashIfEquals(object1: Node, object2: Node): Int {
|
||||
val database = Database.getInstance()
|
||||
|
||||
abstract fun compareBySpecificOrder(object1: T, object2: T): Int
|
||||
|
||||
private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int {
|
||||
val specificOrderComp = compareBySpecificOrder(object1, object2)
|
||||
|
||||
return if (specificOrderComp == 0) {
|
||||
object1.hashCode() - object2.hashCode()
|
||||
} else if (!ascending) -specificOrderComp else specificOrderComp // If descending, revert
|
||||
return when {
|
||||
specificOrderComp == 0 -> object1.hashCode() - object2.hashCode()
|
||||
sortNodeParameters.ascending -> specificOrderComp
|
||||
else -> -specificOrderComp
|
||||
}
|
||||
}
|
||||
|
||||
override fun compare(object1: Node, object2: Node): Int {
|
||||
override fun compare(object1: T, object2: T): Int {
|
||||
if (object1 == object2)
|
||||
return 0
|
||||
|
||||
if (object1.type == Type.GROUP) {
|
||||
return if (object2.type == Type.GROUP) {
|
||||
when (object1.type) {
|
||||
Type.GROUP -> {
|
||||
when (object2.type) {
|
||||
Type.GROUP -> {
|
||||
// RecycleBin at end of groups
|
||||
val database = Database.getInstance()
|
||||
if (database.isRecycleBinEnabled && recycleBinBottom) {
|
||||
if (database.isRecycleBinEnabled && sortNodeParameters.recycleBinBottom) {
|
||||
if (database.recycleBin == object1)
|
||||
return 1
|
||||
if (database.recycleBin == object2)
|
||||
return -1
|
||||
}
|
||||
specificOrderOrHashIfEquals(object1, object2)
|
||||
} else if (object2.type == Type.ENTRY) {
|
||||
if (groupsBefore)
|
||||
return specificOrderOrHashIfEquals(object1, object2)
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
return if (sortNodeParameters.groupsBefore)
|
||||
-1
|
||||
else
|
||||
1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
} else if (object1.type == Type.ENTRY) {
|
||||
return if (object2.type == Type.ENTRY) {
|
||||
specificOrderOrHashIfEquals(object1, object2)
|
||||
} else if (object2.type == Type.GROUP) {
|
||||
if (groupsBefore)
|
||||
}
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
return when (object2.type) {
|
||||
Type.GROUP -> {
|
||||
if (sortNodeParameters.groupsBefore)
|
||||
1
|
||||
else
|
||||
-1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
specificOrderOrHashIfEquals(object1, object2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type not known
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of node by natural database placement
|
||||
*/
|
||||
class NodeNaturalComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
return object1.nodePositionInParent.compareTo(object2.nodePositionInParent)
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
return object1.nodeIndexInParentForNaturalOrder()
|
||||
.compareTo(object2.nodeIndexInParentForNaturalOrder())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of Node by Title
|
||||
*/
|
||||
class NodeTitleComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
return object1.title.compareTo(object2.title, ignoreCase = true)
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
|
||||
return if (titleCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
titleCompare
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of Node by Username, Groups by title
|
||||
*/
|
||||
class NodeUsernameComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
|
||||
// To get username if it's a ref
|
||||
return (object1 as Entry).getEntryInfo(Database.getInstance()).username
|
||||
.compareTo((object2 as Entry).getEntryInfo(Database.getInstance()).username,
|
||||
val usernameCompare = (object1 as Entry).getEntryInfo(database).username
|
||||
.compareTo((object2 as Entry).getEntryInfo(database).username,
|
||||
ignoreCase = true)
|
||||
if (usernameCompare == 0)
|
||||
NodeTitleComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
usernameCompare
|
||||
} else {
|
||||
NodeTitleComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
}
|
||||
return NodeTitleComparator(ascending, groupsBefore, recycleBinBottom).compare(object1, object2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of node by creation
|
||||
*/
|
||||
class NodeCreationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
return object1.creationTime.date
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val creationCompare = object1.creationTime.date
|
||||
.compareTo(object2.creationTime.date)
|
||||
return if (creationCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
creationCompare
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of node by last modification
|
||||
*/
|
||||
class NodeLastModificationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
return object1.lastModificationTime.date
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val lastModificationCompare = object1.lastModificationTime.date
|
||||
.compareTo(object2.lastModificationTime.date)
|
||||
return if (lastModificationCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
lastModificationCompare
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of node by last access
|
||||
*/
|
||||
class NodeLastAccessComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
return object1.lastAccessTime.date
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val lastAccessCompare = object1.lastAccessTime.date
|
||||
.compareTo(object2.lastAccessTime.date)
|
||||
return if (lastAccessCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
lastAccessCompare
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ package com.kunzisoft.keepass.database.element.database
|
||||
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -267,6 +267,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
|
||||
private const val DEFAULT_ENCRYPTION_ROUNDS = 300
|
||||
|
||||
const val BUFFER_SIZE_BYTES = 3 * 128
|
||||
|
||||
/**
|
||||
* Encrypt the master key a few times to make brute-force key-search harder
|
||||
* @throws IOException
|
||||
|
||||
@@ -181,7 +181,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
// To compress, create a new binary with file
|
||||
binary.compress()
|
||||
binary.compress(BUFFER_SIZE_BYTES)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
when (newCompression) {
|
||||
CompressionAlgorithm.None -> {
|
||||
// To decompress, create a new binary with file
|
||||
binary.decompress()
|
||||
binary.decompress(BUFFER_SIZE_BYTES)
|
||||
}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
}
|
||||
@@ -562,7 +562,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
private const val KeyElementName = "Key"
|
||||
private const val KeyDataElementName = "Data"
|
||||
|
||||
const val BASE_64_FLAG = Base64.DEFAULT
|
||||
const val BASE_64_FLAG = Base64.NO_WRAP
|
||||
|
||||
const val BUFFER_SIZE_BYTES = 3 * 128
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -51,19 +52,15 @@ import java.util.*
|
||||
*/
|
||||
class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface {
|
||||
|
||||
/** A string describing what is in pBinaryData */
|
||||
var binaryDesc = ""
|
||||
/**
|
||||
* @return the actual binaryData byte array.
|
||||
*/
|
||||
var binaryData: ByteArray = ByteArray(0)
|
||||
/** A string describing what is in binaryData */
|
||||
var binaryDescription = ""
|
||||
var binaryData: BinaryAttachment? = null
|
||||
|
||||
// Determine if this is a MetaStream entry
|
||||
val isMetaStream: Boolean
|
||||
get() {
|
||||
if (binaryData.contentEquals(ByteArray(0))) return false
|
||||
if (notes.isEmpty()) return false
|
||||
if (binaryDesc != PMS_ID_BINDESC) return false
|
||||
if (binaryDescription != PMS_ID_BINDESC) return false
|
||||
if (title.isEmpty()) return false
|
||||
if (title != PMS_ID_TITLE) return false
|
||||
if (username.isEmpty()) return false
|
||||
@@ -88,9 +85,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
password = parcel.readString() ?: password
|
||||
url = parcel.readString() ?: url
|
||||
notes = parcel.readString() ?: notes
|
||||
binaryDesc = parcel.readString() ?: binaryDesc
|
||||
binaryData = ByteArray(parcel.readInt())
|
||||
parcel.readByteArray(binaryData)
|
||||
binaryDescription = parcel.readString() ?: binaryDescription
|
||||
binaryData = parcel.readParcelable(BinaryAttachment::class.java.classLoader)
|
||||
}
|
||||
|
||||
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
||||
@@ -108,9 +104,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
dest.writeString(password)
|
||||
dest.writeString(url)
|
||||
dest.writeString(notes)
|
||||
dest.writeString(binaryDesc)
|
||||
dest.writeInt(binaryData.size)
|
||||
dest.writeByteArray(binaryData)
|
||||
dest.writeString(binaryDescription)
|
||||
dest.writeParcelable(binaryData, flags)
|
||||
}
|
||||
|
||||
fun updateWith(source: EntryKDB) {
|
||||
@@ -120,11 +115,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
password = source.password
|
||||
url = source.url
|
||||
notes = source.notes
|
||||
binaryDesc = source.binaryDesc
|
||||
|
||||
val descLen = source.binaryData.size
|
||||
binaryData = ByteArray(descLen)
|
||||
System.arraycopy(source.binaryData, 0, binaryData, 0, descLen)
|
||||
binaryDescription = source.binaryDescription
|
||||
binaryData = source.binaryData
|
||||
}
|
||||
|
||||
override var username = ""
|
||||
|
||||
@@ -304,6 +304,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
history.add(entry)
|
||||
}
|
||||
|
||||
fun removeEntryFromHistory(position: Int) {
|
||||
history.removeAt(position)
|
||||
}
|
||||
|
||||
fun removeAllHistory() {
|
||||
history.clear()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.element.entry
|
||||
|
||||
import android.os.Parcel
|
||||
@@ -17,4 +36,14 @@ abstract class EntryVersioned
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel)
|
||||
|
||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||
if (nodeIndexInParentForNaturalOrder == -1) {
|
||||
val numberOfGroups = parent?.getChildGroups()?.size
|
||||
val indexInEntries = parent?.getChildEntries()?.indexOf(this)
|
||||
if (numberOfGroups != null && indexInEntries != null)
|
||||
return numberOfGroups + indexInEntries
|
||||
}
|
||||
return nodeIndexInParentForNaturalOrder
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.element.entry
|
||||
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
/*
|
||||
* 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.database.element.group
|
||||
|
||||
import android.os.Parcel
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import java.util.*
|
||||
|
||||
abstract class GroupVersioned
|
||||
<
|
||||
@@ -15,9 +35,10 @@ abstract class GroupVersioned
|
||||
|
||||
private var titleGroup = ""
|
||||
@Transient
|
||||
private val childGroups = ArrayList<Group>()
|
||||
private val childGroups = LinkedList<Group>()
|
||||
@Transient
|
||||
private val childEntries = ArrayList<Entry>()
|
||||
private val childEntries = LinkedList<Entry>()
|
||||
private var positionIndexChildren = 0
|
||||
|
||||
constructor() : super()
|
||||
|
||||
@@ -33,9 +54,8 @@ abstract class GroupVersioned
|
||||
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>) {
|
||||
super.updateWith(source)
|
||||
titleGroup = source.titleGroup
|
||||
childGroups.clear()
|
||||
removeChildren()
|
||||
childGroups.addAll(source.childGroups)
|
||||
childEntries.clear()
|
||||
childEntries.addAll(source.childEntries)
|
||||
}
|
||||
|
||||
@@ -43,23 +63,27 @@ abstract class GroupVersioned
|
||||
get() = titleGroup
|
||||
set(value) { titleGroup = value }
|
||||
|
||||
override fun getChildGroups(): MutableList<Group> {
|
||||
override fun getChildGroups(): List<Group> {
|
||||
return childGroups
|
||||
}
|
||||
|
||||
override fun getChildEntries(): MutableList<Entry> {
|
||||
override fun getChildEntries(): List<Entry> {
|
||||
return childEntries
|
||||
}
|
||||
|
||||
override fun addChildGroup(group: Group) {
|
||||
if (childGroups.contains(group))
|
||||
removeChildGroup(group)
|
||||
positionIndexChildren++
|
||||
group.nodeIndexInParentForNaturalOrder = positionIndexChildren
|
||||
this.childGroups.add(group)
|
||||
}
|
||||
|
||||
override fun addChildEntry(entry: Entry) {
|
||||
if (childEntries.contains(entry))
|
||||
removeChildEntry(entry)
|
||||
positionIndexChildren++
|
||||
entry.nodeIndexInParentForNaturalOrder = positionIndexChildren
|
||||
this.childEntries.add(entry)
|
||||
}
|
||||
|
||||
@@ -76,6 +100,13 @@ abstract class GroupVersioned
|
||||
this.childEntries.clear()
|
||||
}
|
||||
|
||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||
return if (nodeIndexInParentForNaturalOrder == -1)
|
||||
childGroups.indexOf(this)
|
||||
else
|
||||
nodeIndexInParentForNaturalOrder
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return titleGroup
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.element.group
|
||||
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
@@ -5,9 +24,9 @@ import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
|
||||
interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>, Entry> : NodeVersionedInterface<Group> {
|
||||
|
||||
fun getChildGroups(): MutableList<Group>
|
||||
fun getChildGroups(): List<Group>
|
||||
|
||||
fun getChildEntries(): MutableList<Entry>
|
||||
fun getChildEntries(): List<Entry>
|
||||
|
||||
fun addChildGroup(group: Group)
|
||||
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.element.node
|
||||
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
@@ -6,17 +25,6 @@ interface Node: NodeVersionedInterface<Group> {
|
||||
|
||||
val nodeId: NodeId<*>?
|
||||
|
||||
val nodePositionInParent: Int
|
||||
get() {
|
||||
parent?.getChildren(true)?.let { children ->
|
||||
children.forEachIndexed { index, nodeVersioned ->
|
||||
if (nodeVersioned.nodeId == this.nodeId)
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun addParentFrom(node: Node) {
|
||||
parent = node.parent
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.element.node
|
||||
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
|
||||
@@ -40,6 +40,8 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
|
||||
val id: IdType
|
||||
get() = nodeId.id
|
||||
|
||||
var nodeIndexInParentForNaturalOrder = -1
|
||||
|
||||
protected constructor()
|
||||
|
||||
protected constructor(parcel: Parcel) {
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.element.node
|
||||
|
||||
import android.os.Parcelable
|
||||
@@ -28,5 +47,10 @@ interface NodeVersionedInterface<ParentGroup> : NodeTimeInterface, Parcelable {
|
||||
|
||||
fun isContainedIn(container: ParentGroup): Boolean
|
||||
|
||||
/**
|
||||
* Groups are always before in natural order (DB order)
|
||||
*/
|
||||
fun nodeIndexInParentForNaturalOrder(): Int
|
||||
|
||||
fun touch(modified: Boolean, touchParents: Boolean)
|
||||
}
|
||||
@@ -19,11 +19,11 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.security
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES
|
||||
import com.kunzisoft.keepass.stream.ReadBytes
|
||||
import com.kunzisoft.keepass.stream.readFromStream
|
||||
import com.kunzisoft.keepass.stream.readBytes
|
||||
import java.io.*
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
@@ -73,19 +73,22 @@ class BinaryAttachment : Parcelable {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun compress() {
|
||||
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||
if (dataFile != null) {
|
||||
// To compress, create a new binary with file
|
||||
if (isCompressed != true) {
|
||||
val fileBinaryCompress = File(dataFile!!.parent, dataFile!!.name + "_temp")
|
||||
val outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress))
|
||||
readFromStream(getInputDataStream(), BUFFER_SIZE_BYTES,
|
||||
object : ReadBytes {
|
||||
override fun read(buffer: ByteArray) {
|
||||
var outputStream: GZIPOutputStream? = null
|
||||
var inputStream: InputStream? = null
|
||||
try {
|
||||
outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress))
|
||||
inputStream = getInputDataStream()
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
})
|
||||
outputStream.close()
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
|
||||
// Remove unGzip file
|
||||
if (dataFile!!.delete()) {
|
||||
@@ -97,20 +100,24 @@ class BinaryAttachment : Parcelable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun decompress() {
|
||||
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||
if (dataFile != null) {
|
||||
if (isCompressed != false) {
|
||||
val fileBinaryDecompress = File(dataFile!!.parent, dataFile!!.name + "_temp")
|
||||
val outputStream = FileOutputStream(fileBinaryDecompress)
|
||||
readFromStream(GZIPInputStream(getInputDataStream()), BUFFER_SIZE_BYTES,
|
||||
object : ReadBytes {
|
||||
override fun read(buffer: ByteArray) {
|
||||
var outputStream: FileOutputStream? = null
|
||||
var inputStream: GZIPInputStream? = null
|
||||
try {
|
||||
outputStream = FileOutputStream(fileBinaryDecompress)
|
||||
inputStream = GZIPInputStream(getInputDataStream())
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
})
|
||||
outputStream.close()
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
|
||||
// Remove gzip file
|
||||
if (dataFile!!.delete()) {
|
||||
@@ -122,6 +129,33 @@ class BinaryAttachment : Parcelable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun download(createdFileUri: Uri,
|
||||
contentResolver: ContentResolver,
|
||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||
update: ((percent: Int)->Unit)? = null) {
|
||||
|
||||
var dataDownloaded = 0
|
||||
contentResolver.openOutputStream(createdFileUri).use { outputStream ->
|
||||
outputStream?.let { fileOutputStream ->
|
||||
if (isCompressed == true) {
|
||||
GZIPInputStream(getInputDataStream())
|
||||
} else {
|
||||
getInputDataStream()
|
||||
}.use { inputStream ->
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
fileOutputStream.write(buffer)
|
||||
dataDownloaded += buffer.size
|
||||
try {
|
||||
val percentDownload = (100 * dataDownloaded / length()).toInt()
|
||||
update?.invoke(percentDownload)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun clear() {
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.exception
|
||||
|
||||
import android.content.res.Resources
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.exception
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
|
||||
package com.kunzisoft.keepass.database.file
|
||||
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
|
||||
import com.kunzisoft.keepass.stream.bytes4ToInt
|
||||
import com.kunzisoft.keepass.stream.readBytesLength
|
||||
import com.kunzisoft.keepass.stream.readBytes4ToInt
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
class DatabaseHeaderKDB : DatabaseHeader() {
|
||||
|
||||
@@ -47,30 +49,25 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
||||
*/
|
||||
var contentsHash = ByteArray(32)
|
||||
|
||||
// As UInt
|
||||
var numKeyEncRounds: Int = 0
|
||||
|
||||
/**
|
||||
* Parse given buf, as read from file.
|
||||
* @param buf
|
||||
* @throws IOException
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun loadFromFile(buf: ByteArray, offset: Int) {
|
||||
signature1 = LEDataInputStream.readInt(buf, offset)
|
||||
signature2 = LEDataInputStream.readInt(buf, offset + 4)
|
||||
flags = LEDataInputStream.readInt(buf, offset + 8)
|
||||
version = LEDataInputStream.readInt(buf, offset + 12)
|
||||
|
||||
System.arraycopy(buf, offset + 16, masterSeed, 0, 16)
|
||||
System.arraycopy(buf, offset + 32, encryptionIV, 0, 16)
|
||||
|
||||
numGroups = LEDataInputStream.readInt(buf, offset + 48)
|
||||
numEntries = LEDataInputStream.readInt(buf, offset + 52)
|
||||
|
||||
System.arraycopy(buf, offset + 56, contentsHash, 0, 32)
|
||||
|
||||
System.arraycopy(buf, offset + 88, transformSeed, 0, 32)
|
||||
numKeyEncRounds = LEDataInputStream.readInt(buf, offset + 120)
|
||||
fun loadFromFile(inputStream: InputStream) {
|
||||
signature1 = inputStream.readBytes4ToInt() // 4 bytes
|
||||
signature2 = inputStream.readBytes4ToInt() // 4 bytes
|
||||
flags = inputStream.readBytes4ToInt() // 4 bytes
|
||||
version = inputStream.readBytes4ToInt() // 4 bytes
|
||||
masterSeed = inputStream.readBytesLength(16) // 16 bytes
|
||||
encryptionIV = inputStream.readBytesLength(16) // 16 bytes
|
||||
numGroups = inputStream.readBytes4ToInt() // 4 bytes
|
||||
numEntries = inputStream.readBytes4ToInt() // 4 bytes
|
||||
contentsHash = inputStream.readBytesLength(32) // 32 bytes
|
||||
transformSeed = inputStream.readBytesLength(32) // 32 bytes
|
||||
numKeyEncRounds = inputStream.readBytes4ToInt()
|
||||
if (numKeyEncRounds < 0) {
|
||||
// TODO: Really treat this like an unsigned integer
|
||||
throw IOException("Does not support more than " + Integer.MAX_VALUE + " rounds.")
|
||||
|
||||
@@ -30,10 +30,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||
import com.kunzisoft.keepass.stream.CopyInputStream
|
||||
import com.kunzisoft.keepass.stream.HmacBlockStream
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -148,7 +145,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
val headerBOS = ByteArrayOutputStream()
|
||||
val copyInputStream = CopyInputStream(inputStream, headerBOS)
|
||||
val digestInputStream = DigestInputStream(copyInputStream, messageDigest)
|
||||
val littleEndianDataInputStream = LEDataInputStream(digestInputStream)
|
||||
val littleEndianDataInputStream = LittleEndianDataInputStream(digestInputStream)
|
||||
|
||||
val sig1 = littleEndianDataInputStream.readInt()
|
||||
val sig2 = littleEndianDataInputStream.readInt()
|
||||
@@ -172,7 +169,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun readHeaderField(dis: LEDataInputStream): Boolean {
|
||||
private fun readHeaderField(dis: LittleEndianDataInputStream): Boolean {
|
||||
val fieldID = dis.read().toByte()
|
||||
|
||||
val fieldSize: Int = if (version < FILE_VERSION_32_4) {
|
||||
@@ -243,12 +240,12 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
throw IOException("Invalid cipher ID.")
|
||||
}
|
||||
|
||||
databaseV4.dataCipher = DatabaseInputOutputUtils.bytesToUuid(pbId)
|
||||
databaseV4.dataCipher = bytes16ToUuid(pbId)
|
||||
}
|
||||
|
||||
private fun setTransformRound(roundsByte: ByteArray?) {
|
||||
private fun setTransformRound(roundsByte: ByteArray) {
|
||||
assignAesKdfEngineIfNotExists()
|
||||
val rounds = LEDataInputStream.readLong(roundsByte!!, 0)
|
||||
val rounds = bytes64ToLong(roundsByte)
|
||||
databaseV4.kdfParameters?.setUInt64(AesKdf.PARAM_ROUNDS, rounds)
|
||||
databaseV4.numberKeyEncryptionRounds = rounds
|
||||
}
|
||||
@@ -259,7 +256,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
throw IOException("Invalid compression flags.")
|
||||
}
|
||||
|
||||
val flag = LEDataInputStream.readInt(pbFlags, 0)
|
||||
val flag = bytes4ToInt(pbFlags)
|
||||
if (flag < 0 || flag >= CompressionAlgorithm.values().size) {
|
||||
throw IOException("Unrecognized compression flag.")
|
||||
}
|
||||
@@ -275,7 +272,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
throw IOException("Invalid stream id.")
|
||||
}
|
||||
|
||||
val id = LEDataInputStream.readInt(streamID, 0)
|
||||
val id = bytes4ToInt(streamID)
|
||||
if (id < 0 || id >= CrsAlgorithm.values().size) {
|
||||
throw IOException("Invalid stream id.")
|
||||
}
|
||||
@@ -295,6 +292,9 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
var ULONG_MAX_VALUE: Long = -1
|
||||
|
||||
const val DBSIG_PRE2 = -0x4ab4049a
|
||||
const val DBSIG_2 = -0x4ab40499
|
||||
|
||||
@@ -323,7 +323,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun computeHeaderHmac(header: ByteArray, key: ByteArray): ByteArray {
|
||||
val blockKey = HmacBlockStream.GetHmacKey64(key, DatabaseInputOutputUtils.ULONG_MAX_VALUE)
|
||||
val blockKey = HmacBlockStream.getHmacKey64(key, ULONG_MAX_VALUE)
|
||||
|
||||
val hmac: Mac
|
||||
try {
|
||||
|
||||
@@ -22,9 +22,11 @@ package com.kunzisoft.keepass.database.file.input
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>> {
|
||||
abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
|
||||
(protected val cacheDirectory: File) {
|
||||
|
||||
/**
|
||||
* Load a versioned database file, return contents in a new DatabaseVersioned.
|
||||
|
||||
@@ -16,36 +16,10 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
|
||||
Derived from
|
||||
|
||||
KeePass for J2ME
|
||||
|
||||
Copyright 2007 Naomaru Itoi <nao@phoneid.org>
|
||||
|
||||
This file was derived from
|
||||
|
||||
Java clone of KeePass - A KeePass file viewer for Java
|
||||
Copyright 2006 Bill Zwicky <billzwicky@users.sourceforge.net>
|
||||
|
||||
This program 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; version 2
|
||||
|
||||
This program 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 this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
package com.kunzisoft.keepass.database.file.input
|
||||
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
@@ -53,28 +27,29 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
|
||||
import javax.crypto.*
|
||||
import org.joda.time.Instant
|
||||
import java.io.*
|
||||
import java.security.*
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.security.*
|
||||
import java.util.Arrays
|
||||
|
||||
|
||||
/**
|
||||
* Load a KDB database file.
|
||||
*/
|
||||
class DatabaseInputKDB : DatabaseInput<DatabaseKDB>() {
|
||||
class DatabaseInputKDB(cacheDirectory: File,
|
||||
private val fixDuplicateUUID: Boolean = false)
|
||||
: DatabaseInput<DatabaseKDB>(cacheDirectory) {
|
||||
|
||||
private lateinit var mDatabaseToOpen: DatabaseKDB
|
||||
|
||||
@@ -87,52 +62,63 @@ class DatabaseInputKDB : DatabaseInput<DatabaseKDB>() {
|
||||
try {
|
||||
// Load entire file, most of it's encrypted.
|
||||
val fileSize = databaseInputStream.available()
|
||||
val filebuf = ByteArray(fileSize + 16) // Pad with a blocksize (Twofish uses 128 bits), since Android 4.3 tries to write more to the buffer
|
||||
databaseInputStream.read(filebuf, 0, fileSize) // TODO remove
|
||||
databaseInputStream.close()
|
||||
|
||||
// Parse header (unencrypted)
|
||||
if (fileSize < DatabaseHeaderKDB.BUF_SIZE)
|
||||
throw IOException("File too short for header")
|
||||
val hdr = DatabaseHeaderKDB()
|
||||
hdr.loadFromFile(filebuf, 0)
|
||||
val header = DatabaseHeaderKDB()
|
||||
header.loadFromFile(databaseInputStream)
|
||||
|
||||
if (hdr.signature1 != DatabaseHeader.PWM_DBSIG_1 || hdr.signature2 != DatabaseHeaderKDB.DBSIG_2) {
|
||||
val contentSize = databaseInputStream.available()
|
||||
if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE))
|
||||
throw IOException("Header corrupted")
|
||||
|
||||
if (header.signature1 != DatabaseHeader.PWM_DBSIG_1
|
||||
|| header.signature2 != DatabaseHeaderKDB.DBSIG_2) {
|
||||
throw SignatureDatabaseException()
|
||||
}
|
||||
|
||||
if (!hdr.matchesVersion()) {
|
||||
if (!header.matchesVersion()) {
|
||||
throw VersionDatabaseException()
|
||||
}
|
||||
|
||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||
mDatabaseToOpen = DatabaseKDB()
|
||||
|
||||
mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID
|
||||
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
|
||||
|
||||
// Select algorithm
|
||||
when {
|
||||
hdr.flags and DatabaseHeaderKDB.FLAG_RIJNDAEL != 0 -> mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
|
||||
hdr.flags and DatabaseHeaderKDB.FLAG_TWOFISH != 0 -> mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish
|
||||
header.flags and DatabaseHeaderKDB.FLAG_RIJNDAEL != 0 -> {
|
||||
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
|
||||
}
|
||||
header.flags and DatabaseHeaderKDB.FLAG_TWOFISH != 0 -> {
|
||||
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish
|
||||
}
|
||||
else -> throw InvalidAlgorithmDatabaseException()
|
||||
}
|
||||
|
||||
mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong()
|
||||
mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toLong()
|
||||
|
||||
// Generate transformedMasterKey from masterKey
|
||||
mDatabaseToOpen.makeFinalKey(hdr.masterSeed, hdr.transformSeed, mDatabaseToOpen.numberKeyEncryptionRounds)
|
||||
mDatabaseToOpen.makeFinalKey(
|
||||
header.masterSeed,
|
||||
header.transformSeed,
|
||||
mDatabaseToOpen.numberKeyEncryptionRounds)
|
||||
|
||||
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
|
||||
// Initialize Rijndael algorithm
|
||||
val cipher: Cipher
|
||||
try {
|
||||
if (mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael) {
|
||||
cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
||||
} else if (mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.Twofish) {
|
||||
cipher = CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
|
||||
} else {
|
||||
throw IOException("Encryption algorithm is not supported")
|
||||
val cipher: Cipher = try {
|
||||
when {
|
||||
mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
|
||||
CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
||||
}
|
||||
mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
|
||||
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
|
||||
}
|
||||
else -> throw IOException("Encryption algorithm is not supported")
|
||||
}
|
||||
|
||||
} catch (e1: NoSuchAlgorithmException) {
|
||||
throw IOException("No such algorithm")
|
||||
} catch (e1: NoSuchPaddingException) {
|
||||
@@ -140,98 +126,231 @@ class DatabaseInputKDB : DatabaseInput<DatabaseKDB>() {
|
||||
}
|
||||
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(mDatabaseToOpen.finalKey, "AES"), IvParameterSpec(hdr.encryptionIV))
|
||||
cipher.init(Cipher.DECRYPT_MODE,
|
||||
SecretKeySpec(mDatabaseToOpen.finalKey, "AES"),
|
||||
IvParameterSpec(header.encryptionIV))
|
||||
} catch (e1: InvalidKeyException) {
|
||||
throw IOException("Invalid key")
|
||||
} catch (e1: InvalidAlgorithmParameterException) {
|
||||
throw IOException("Invalid algorithm parameter.")
|
||||
}
|
||||
|
||||
// Decrypt! The first bytes aren't encrypted (that's the header)
|
||||
val encryptedPartSize: Int
|
||||
val messageDigest: MessageDigest
|
||||
try {
|
||||
encryptedPartSize = cipher.doFinal(filebuf, DatabaseHeaderKDB.BUF_SIZE, fileSize - DatabaseHeaderKDB.BUF_SIZE, filebuf, DatabaseHeaderKDB.BUF_SIZE)
|
||||
} catch (e1: ShortBufferException) {
|
||||
throw IOException("Buffer too short")
|
||||
} catch (e1: IllegalBlockSizeException) {
|
||||
throw IOException("Invalid block size")
|
||||
} catch (e1: BadPaddingException) {
|
||||
throw InvalidCredentialsDatabaseException()
|
||||
}
|
||||
|
||||
val md: MessageDigest
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256")
|
||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("No SHA-256 algorithm")
|
||||
}
|
||||
|
||||
val nos = NullOutputStream()
|
||||
val dos = DigestOutputStream(nos, md)
|
||||
dos.write(filebuf, DatabaseHeaderKDB.BUF_SIZE, encryptedPartSize)
|
||||
dos.close()
|
||||
val hash = md.digest()
|
||||
// Decrypt content
|
||||
val cipherInputStream = BufferedInputStream(
|
||||
DigestInputStream(
|
||||
BetterCipherInputStream(databaseInputStream, cipher),
|
||||
messageDigest
|
||||
)
|
||||
)
|
||||
|
||||
if (!Arrays.equals(hash, hdr.contentsHash)) {
|
||||
/* TODO checksum
|
||||
// Add a mark to the content start
|
||||
if (!cipherInputStream.markSupported()) {
|
||||
throw IOException("Input stream does not support mark.")
|
||||
}
|
||||
cipherInputStream.mark(cipherInputStream.available() +1)
|
||||
// Consume all data to get the digest
|
||||
var numberRead = 0
|
||||
while (numberRead > -1) {
|
||||
numberRead = cipherInputStream.read(ByteArray(1024))
|
||||
}
|
||||
|
||||
Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)")
|
||||
// Check sum
|
||||
if (!Arrays.equals(messageDigest.digest(), header.contentsHash)) {
|
||||
throw InvalidCredentialsDatabaseException()
|
||||
}
|
||||
// Back to the content start
|
||||
cipherInputStream.reset()
|
||||
*/
|
||||
|
||||
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
||||
val newRoot = mDatabaseToOpen.createGroup()
|
||||
newRoot.level = -1
|
||||
mDatabaseToOpen.rootGroup = newRoot
|
||||
|
||||
// Import all groups
|
||||
var pos = DatabaseHeaderKDB.BUF_SIZE
|
||||
var newGrp = mDatabaseToOpen.createGroup()
|
||||
run {
|
||||
var i = 0
|
||||
while (i < hdr.numGroups) {
|
||||
val fieldType = LEDataInputStream.readUShort(filebuf, pos)
|
||||
pos += 2
|
||||
val fieldSize = LEDataInputStream.readInt(filebuf, pos)
|
||||
pos += 4
|
||||
// Import all nodes
|
||||
var newGroup: GroupKDB? = null
|
||||
var newEntry: EntryKDB? = null
|
||||
var currentGroupNumber = 0
|
||||
var currentEntryNumber = 0
|
||||
while (currentGroupNumber < header.numGroups
|
||||
|| currentEntryNumber < header.numEntries) {
|
||||
|
||||
if (fieldType == 0xFFFF) {
|
||||
// End-Group record. Save group and count it.
|
||||
mDatabaseToOpen.addGroupIndex(newGrp)
|
||||
newGrp = mDatabaseToOpen.createGroup()
|
||||
i++
|
||||
} else {
|
||||
readGroupField(mDatabaseToOpen, newGrp, fieldType, filebuf, pos)
|
||||
}
|
||||
pos += fieldSize
|
||||
}
|
||||
}
|
||||
val fieldType = cipherInputStream.readBytes2ToUShort()
|
||||
val fieldSize = cipherInputStream.readBytes4ToUInt().toInt()
|
||||
|
||||
// Import all entries
|
||||
var newEnt = mDatabaseToOpen.createEntry()
|
||||
var i = 0
|
||||
while (i < hdr.numEntries) {
|
||||
val fieldType = LEDataInputStream.readUShort(filebuf, pos)
|
||||
val fieldSize = LEDataInputStream.readInt(filebuf, pos + 2)
|
||||
|
||||
if (fieldType == 0xFFFF) {
|
||||
// End-Group record. Save group and count it.
|
||||
mDatabaseToOpen.addEntryIndex(newEnt)
|
||||
newEnt = mDatabaseToOpen.createEntry()
|
||||
i++
|
||||
} else {
|
||||
readEntryField(mDatabaseToOpen, newEnt, filebuf, pos)
|
||||
when (fieldType) {
|
||||
0x0000 -> {
|
||||
cipherInputStream.readBytesLength(fieldSize)
|
||||
}
|
||||
pos += 2 + 4 + fieldSize
|
||||
0x0001 -> {
|
||||
// Create new node depending on byte number
|
||||
when (fieldSize) {
|
||||
4 -> {
|
||||
newGroup = mDatabaseToOpen.createGroup().apply {
|
||||
setGroupId(cipherInputStream.readBytes4ToInt())
|
||||
}
|
||||
}
|
||||
16 -> {
|
||||
newEntry = mDatabaseToOpen.createEntry().apply {
|
||||
nodeId = NodeIdUUID(cipherInputStream.readBytes16ToUuid())
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
throw UnsupportedEncodingException("Field type $fieldType")
|
||||
}
|
||||
}
|
||||
}
|
||||
0x0002 -> {
|
||||
newGroup?.let { group ->
|
||||
group.title = cipherInputStream.readBytesToString(fieldSize)
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
val groupKDB = mDatabaseToOpen.createGroup()
|
||||
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToInt())
|
||||
entry.parent = groupKDB
|
||||
}
|
||||
}
|
||||
0x0003 -> {
|
||||
newGroup?.let { group ->
|
||||
group.creationTime = cipherInputStream.readBytes5ToDate()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
var iconId = cipherInputStream.readBytes4ToInt()
|
||||
// Clean up after bug that set icon ids to -1
|
||||
if (iconId == -1) {
|
||||
iconId = 0
|
||||
}
|
||||
entry.icon = mDatabaseToOpen.iconFactory.getIcon(iconId)
|
||||
}
|
||||
}
|
||||
0x0004 -> {
|
||||
newGroup?.let { group ->
|
||||
group.lastModificationTime = cipherInputStream.readBytes5ToDate()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
entry.title = cipherInputStream.readBytesToString(fieldSize)
|
||||
}
|
||||
}
|
||||
0x0005 -> {
|
||||
newGroup?.let { group ->
|
||||
group.lastAccessTime = cipherInputStream.readBytes5ToDate()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
entry.url = cipherInputStream.readBytesToString(fieldSize)
|
||||
}
|
||||
}
|
||||
0x0006 -> {
|
||||
newGroup?.let { group ->
|
||||
group.expiryTime = cipherInputStream.readBytes5ToDate()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
entry.username = cipherInputStream.readBytesToString(fieldSize)
|
||||
}
|
||||
}
|
||||
0x0007 -> {
|
||||
newGroup?.let { group ->
|
||||
group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToInt())
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
entry.password = cipherInputStream.readBytesToString(fieldSize,false)
|
||||
}
|
||||
}
|
||||
0x0008 -> {
|
||||
newGroup?.let { group ->
|
||||
group.level = cipherInputStream.readBytes2ToUShort()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
entry.notes = cipherInputStream.readBytesToString(fieldSize)
|
||||
}
|
||||
}
|
||||
0x0009 -> {
|
||||
newGroup?.let { group ->
|
||||
group.flags = cipherInputStream.readBytes4ToInt()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
entry.creationTime = cipherInputStream.readBytes5ToDate()
|
||||
}
|
||||
}
|
||||
0x000A -> {
|
||||
newEntry?.let { entry ->
|
||||
entry.lastModificationTime = cipherInputStream.readBytes5ToDate()
|
||||
}
|
||||
}
|
||||
0x000B -> {
|
||||
newEntry?.let { entry ->
|
||||
entry.lastAccessTime = cipherInputStream.readBytes5ToDate()
|
||||
}
|
||||
}
|
||||
0x000C -> {
|
||||
newEntry?.let { entry ->
|
||||
entry.expiryTime = cipherInputStream.readBytes5ToDate()
|
||||
}
|
||||
}
|
||||
0x000D -> {
|
||||
newEntry?.let { entry ->
|
||||
entry.binaryDescription = cipherInputStream.readBytesToString(fieldSize)
|
||||
}
|
||||
}
|
||||
0x000E -> {
|
||||
newEntry?.let { entry ->
|
||||
if (fieldSize > 0) {
|
||||
// Generate an unique new file with timestamp
|
||||
val binaryFile = File(cacheDirectory,
|
||||
Instant.now().millis.toString())
|
||||
entry.binaryData = BinaryAttachment(binaryFile)
|
||||
BufferedOutputStream(FileOutputStream(binaryFile)).use { outputStream ->
|
||||
cipherInputStream.readBytes(fieldSize,
|
||||
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
0xFFFF -> {
|
||||
// End record. Save node and count it.
|
||||
newGroup?.let { group ->
|
||||
mDatabaseToOpen.addGroupIndex(group)
|
||||
currentGroupNumber++
|
||||
newGroup = null
|
||||
}
|
||||
newEntry?.let { entry ->
|
||||
mDatabaseToOpen.addEntryIndex(entry)
|
||||
currentEntryNumber++
|
||||
newEntry = null
|
||||
}
|
||||
cipherInputStream.readBytesLength(fieldSize)
|
||||
}
|
||||
else -> {
|
||||
throw UnsupportedEncodingException("Field type $fieldType")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check sum
|
||||
if (!Arrays.equals(messageDigest.digest(), header.contentsHash)) {
|
||||
throw InvalidCredentialsDatabaseException()
|
||||
}
|
||||
|
||||
constructTreeFromIndex()
|
||||
|
||||
} catch (e: LoadDatabaseException) {
|
||||
mDatabaseToOpen.clearCache()
|
||||
throw e
|
||||
} catch (e: IOException) {
|
||||
mDatabaseToOpen.clearCache()
|
||||
throw IODatabaseException(e)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
mDatabaseToOpen.clearCache()
|
||||
throw NoMemoryDatabaseException(e)
|
||||
} catch (e: Exception) {
|
||||
mDatabaseToOpen.clearCache()
|
||||
throw LoadDatabaseException(e)
|
||||
}
|
||||
|
||||
@@ -279,71 +398,6 @@ class DatabaseInputKDB : DatabaseInput<DatabaseKDB>() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and save one record from binary file.
|
||||
* @param buf
|
||||
* @param offset
|
||||
* @return If >0,
|
||||
* @throws UnsupportedEncodingException
|
||||
*/
|
||||
@Throws(UnsupportedEncodingException::class)
|
||||
private fun readGroupField(db: DatabaseKDB, grp: GroupKDB, fieldType: Int, buf: ByteArray, offset: Int) {
|
||||
when (fieldType) {
|
||||
0x0000 -> {
|
||||
}
|
||||
0x0001 -> grp.setGroupId(LEDataInputStream.readInt(buf, offset))
|
||||
0x0002 -> grp.title = DatabaseInputOutputUtils.readCString(buf, offset)
|
||||
0x0003 -> grp.creationTime = DatabaseInputOutputUtils.readCDate(buf, offset)
|
||||
0x0004 -> grp.lastModificationTime = DatabaseInputOutputUtils.readCDate(buf, offset)
|
||||
0x0005 -> grp.lastAccessTime = DatabaseInputOutputUtils.readCDate(buf, offset)
|
||||
0x0006 -> grp.expiryTime = DatabaseInputOutputUtils.readCDate(buf, offset)
|
||||
0x0007 -> grp.icon = db.iconFactory.getIcon(LEDataInputStream.readInt(buf, offset))
|
||||
0x0008 -> grp.level = LEDataInputStream.readUShort(buf, offset)
|
||||
0x0009 -> grp.flags = LEDataInputStream.readInt(buf, offset)
|
||||
}// Ignore field
|
||||
}
|
||||
|
||||
@Throws(UnsupportedEncodingException::class)
|
||||
private fun readEntryField(db: DatabaseKDB, ent: EntryKDB, buf: ByteArray, offset: Int) {
|
||||
var offsetMutable = offset
|
||||
val fieldType = LEDataInputStream.readUShort(buf, offsetMutable)
|
||||
offsetMutable += 2
|
||||
val fieldSize = LEDataInputStream.readInt(buf, offsetMutable)
|
||||
offsetMutable += 4
|
||||
|
||||
when (fieldType) {
|
||||
0x0000 -> {
|
||||
}
|
||||
0x0001 -> ent.nodeId = NodeIdUUID(LEDataInputStream.readUuid(buf, offsetMutable))
|
||||
0x0002 -> {
|
||||
val groupKDB = mDatabaseToOpen.createGroup()
|
||||
groupKDB.nodeId = NodeIdInt(LEDataInputStream.readInt(buf, offsetMutable))
|
||||
ent.parent = groupKDB
|
||||
}
|
||||
0x0003 -> {
|
||||
var iconId = LEDataInputStream.readInt(buf, offsetMutable)
|
||||
|
||||
// Clean up after bug that set icon ids to -1
|
||||
if (iconId == -1) {
|
||||
iconId = 0
|
||||
}
|
||||
|
||||
ent.icon = db.iconFactory.getIcon(iconId)
|
||||
}
|
||||
0x0004 -> ent.title = DatabaseInputOutputUtils.readCString(buf, offsetMutable)
|
||||
0x0005 -> ent.url = DatabaseInputOutputUtils.readCString(buf, offsetMutable)
|
||||
0x0006 -> ent.username = DatabaseInputOutputUtils.readCString(buf, offsetMutable)
|
||||
0x0007 -> ent.password = DatabaseInputOutputUtils.readPassword(buf, offsetMutable)
|
||||
0x0008 -> ent.notes = DatabaseInputOutputUtils.readCString(buf, offsetMutable)
|
||||
0x0009 -> ent.creationTime = DatabaseInputOutputUtils.readCDate(buf, offsetMutable)
|
||||
0x000A -> ent.lastModificationTime = DatabaseInputOutputUtils.readCDate(buf, offsetMutable)
|
||||
0x000B -> ent.lastAccessTime = DatabaseInputOutputUtils.readCDate(buf, offsetMutable)
|
||||
0x000C -> ent.expiryTime = DatabaseInputOutputUtils.readCDate(buf, offsetMutable)
|
||||
0x000D -> ent.binaryDesc = DatabaseInputOutputUtils.readCString(buf, offsetMutable)
|
||||
0x000E -> ent.binaryData = DatabaseInputOutputUtils.readBytes(buf, offsetMutable, fieldSize)
|
||||
}// Ignore field
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DatabaseInputKDB::class.java.name
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||
@@ -37,12 +38,11 @@ import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
|
||||
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
import org.spongycastle.crypto.StreamCipher
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
@@ -54,10 +54,12 @@ import java.util.*
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherInputStream
|
||||
import kotlin.math.min
|
||||
|
||||
class DatabaseInputKDBX(private val streamDir: File,
|
||||
private val fixDuplicateUUID: Boolean = false) : DatabaseInput<DatabaseKDBX>() {
|
||||
class DatabaseInputKDBX(cacheDirectory: File,
|
||||
private val fixDuplicateUUID: Boolean = false)
|
||||
: DatabaseInput<DatabaseKDBX>(cacheDirectory) {
|
||||
|
||||
private var randomStream: StreamCipher? = null
|
||||
private lateinit var mDatabase: DatabaseKDBX
|
||||
@@ -131,11 +133,11 @@ class DatabaseInputKDBX(private val streamDir: File,
|
||||
if (mDatabase.kdbxVersion < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
|
||||
|
||||
val decrypted = attachCipherStream(databaseInputStream, cipher)
|
||||
val dataDecrypted = LEDataInputStream(decrypted)
|
||||
val dataDecrypted = LittleEndianDataInputStream(decrypted)
|
||||
val storedStartBytes: ByteArray?
|
||||
try {
|
||||
storedStartBytes = dataDecrypted.readBytes(32)
|
||||
if (storedStartBytes == null || storedStartBytes.size != 32) {
|
||||
if (storedStartBytes.size != 32) {
|
||||
throw InvalidCredentialsDatabaseException()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
@@ -148,7 +150,7 @@ class DatabaseInputKDBX(private val streamDir: File,
|
||||
|
||||
isPlain = HashedBlockInputStream(dataDecrypted)
|
||||
} else { // KDBX 4
|
||||
val isData = LEDataInputStream(databaseInputStream)
|
||||
val isData = LittleEndianDataInputStream(databaseInputStream)
|
||||
val storedHash = isData.readBytes(32)
|
||||
if (!Arrays.equals(storedHash, hashOfHeader)) {
|
||||
throw InvalidCredentialsDatabaseException()
|
||||
@@ -157,7 +159,7 @@ class DatabaseInputKDBX(private val streamDir: File,
|
||||
val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
|
||||
val headerHmac = DatabaseHeaderKDBX.computeHeaderHmac(pbHeader, hmacKey)
|
||||
val storedHmac = isData.readBytes(32)
|
||||
if (storedHmac == null || storedHmac.size != 32) {
|
||||
if (storedHmac.size != 32) {
|
||||
throw InvalidCredentialsDatabaseException()
|
||||
}
|
||||
// Mac doesn't match
|
||||
@@ -207,12 +209,12 @@ class DatabaseInputKDBX(private val streamDir: File,
|
||||
}
|
||||
|
||||
private fun attachCipherStream(inputStream: InputStream, cipher: Cipher): InputStream {
|
||||
return BetterCipherInputStream(inputStream, cipher, 50 * 1024)
|
||||
return CipherInputStream(inputStream, cipher)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun loadInnerHeader(inputStream: InputStream, header: DatabaseHeaderKDBX) {
|
||||
val lis = LEDataInputStream(inputStream)
|
||||
val lis = LittleEndianDataInputStream(inputStream)
|
||||
|
||||
while (true) {
|
||||
if (!readInnerHeader(lis, header)) break
|
||||
@@ -220,7 +222,8 @@ class DatabaseInputKDBX(private val streamDir: File,
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun readInnerHeader(dataInputStream: LEDataInputStream, header: DatabaseHeaderKDBX): Boolean {
|
||||
private fun readInnerHeader(dataInputStream: LittleEndianDataInputStream,
|
||||
header: DatabaseHeaderKDBX): Boolean {
|
||||
val fieldId = dataInputStream.read().toByte()
|
||||
|
||||
val size = dataInputStream.readInt()
|
||||
@@ -243,13 +246,11 @@ class DatabaseInputKDBX(private val streamDir: File,
|
||||
val protectedFlag = flag && DatabaseHeaderKDBX.KdbxBinaryFlags.Protected.toInt() != DatabaseHeaderKDBX.KdbxBinaryFlags.None.toInt()
|
||||
val byteLength = size - 1
|
||||
// Read in a file
|
||||
val file = File(streamDir, unusedCacheFileName)
|
||||
val file = File(cacheDirectory, unusedCacheFileName)
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
dataInputStream.readBytes(byteLength, object : ReadBytes {
|
||||
override fun read(buffer: ByteArray) {
|
||||
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
})
|
||||
}
|
||||
val protectedBinary = BinaryAttachment(file, protectedFlag)
|
||||
mDatabase.binaryPool.add(protectedBinary)
|
||||
@@ -822,7 +823,7 @@ class DatabaseInputKDBX(private val streamDir: File,
|
||||
buf = buf8
|
||||
}
|
||||
|
||||
val seconds = LEDataInputStream.readLong(buf, 0)
|
||||
val seconds = bytes64ToLong(buf)
|
||||
utcDate = DateKDBXUtil.convertKDBX4Time(seconds)
|
||||
|
||||
} else {
|
||||
@@ -882,7 +883,7 @@ class DatabaseInputKDBX(private val streamDir: File,
|
||||
}
|
||||
val buf = Base64.decode(encoded, BASE_64_FLAG)
|
||||
|
||||
return DatabaseInputOutputUtils.bytesToUuid(buf)
|
||||
return bytes16ToUuid(buf)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
@@ -960,7 +961,7 @@ class DatabaseInputKDBX(private val streamDir: File,
|
||||
|
||||
// New binary to retrieve
|
||||
else {
|
||||
var compressed: Boolean? = null
|
||||
var compressed = false
|
||||
var protected = false
|
||||
|
||||
if (xpp.attributeCount > 0) {
|
||||
@@ -980,16 +981,16 @@ class DatabaseInputKDBX(private val streamDir: File,
|
||||
return BinaryAttachment()
|
||||
val data = Base64.decode(base64, BASE_64_FLAG)
|
||||
|
||||
val file = File(streamDir, unusedCacheFileName)
|
||||
val file = File(cacheDirectory, unusedCacheFileName)
|
||||
return FileOutputStream(file).use { outputStream ->
|
||||
// Force compression in this specific case
|
||||
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||
&& compressed == false) {
|
||||
&& !compressed) {
|
||||
GZIPOutputStream(outputStream).write(data)
|
||||
BinaryAttachment(file, protected, true)
|
||||
} else {
|
||||
outputStream.write(data)
|
||||
BinaryAttachment(file, protected)
|
||||
BinaryAttachment(file, protected, compressed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,34 +20,34 @@
|
||||
package com.kunzisoft.keepass.database.file.output
|
||||
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
|
||||
import com.kunzisoft.keepass.stream.intTo4Bytes
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
class DatabaseHeaderOutputKDB(private val mHeader: DatabaseHeaderKDB, private val mOS: OutputStream) {
|
||||
class DatabaseHeaderOutputKDB(private val mHeader: DatabaseHeaderKDB,
|
||||
private val mOutputStream: OutputStream) {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun outputStart() {
|
||||
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.signature1))
|
||||
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.signature2))
|
||||
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.flags))
|
||||
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.version))
|
||||
mOS.write(mHeader.masterSeed)
|
||||
mOS.write(mHeader.encryptionIV)
|
||||
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.numGroups))
|
||||
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.numEntries))
|
||||
mOutputStream.write(intTo4Bytes(mHeader.signature1))
|
||||
mOutputStream.write(intTo4Bytes(mHeader.signature2))
|
||||
mOutputStream.write(intTo4Bytes(mHeader.flags))
|
||||
mOutputStream.write(intTo4Bytes(mHeader.version))
|
||||
mOutputStream.write(mHeader.masterSeed)
|
||||
mOutputStream.write(mHeader.encryptionIV)
|
||||
mOutputStream.write(intTo4Bytes(mHeader.numGroups))
|
||||
mOutputStream.write(intTo4Bytes(mHeader.numEntries))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun outputContentHash() {
|
||||
mOS.write(mHeader.contentsHash)
|
||||
mOutputStream.write(mHeader.contentsHash)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun outputEnd() {
|
||||
mOS.write(mHeader.transformSeed)
|
||||
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.numKeyEncRounds))
|
||||
mOutputStream.write(mHeader.transformSeed)
|
||||
mOutputStream.write(intTo4Bytes(mHeader.numKeyEncRounds))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
@@ -59,6 +59,6 @@ class DatabaseHeaderOutputKDB(private val mHeader: DatabaseHeaderKDB, private va
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun close() {
|
||||
mOS.close()
|
||||
mOutputStream.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,17 +19,14 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.file.output
|
||||
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.stream.HmacBlockStream
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.MacOutputStream
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.ULONG_MAX_VALUE
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
@@ -37,13 +34,15 @@ import java.security.DigestOutputStream
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class DatabaseHeaderOutputKDBX @Throws(DatabaseOutputException::class)
|
||||
constructor(private val db: DatabaseKDBX, private val header: DatabaseHeaderKDBX, os: OutputStream) : DatabaseHeaderOutput() {
|
||||
private val los: LEDataOutputStream
|
||||
constructor(private val databaseKDBX: DatabaseKDBX,
|
||||
private val header: DatabaseHeaderKDBX,
|
||||
outputStream: OutputStream) : DatabaseHeaderOutput() {
|
||||
|
||||
private val los: LittleEndianDataOutputStream
|
||||
private val mos: MacOutputStream
|
||||
private val dos: DigestOutputStream
|
||||
lateinit var headerHmac: ByteArray
|
||||
@@ -58,15 +57,16 @@ constructor(private val db: DatabaseKDBX, private val header: DatabaseHeaderKDBX
|
||||
}
|
||||
|
||||
try {
|
||||
db.makeFinalKey(header.masterSeed)
|
||||
databaseKDBX.makeFinalKey(header.masterSeed)
|
||||
} catch (e: IOException) {
|
||||
throw DatabaseOutputException(e)
|
||||
}
|
||||
|
||||
val hmacKey = databaseKDBX.hmacKey ?: throw DatabaseOutputException("HmacKey is not defined")
|
||||
val hmac: Mac
|
||||
try {
|
||||
hmac = Mac.getInstance("HmacSHA256")
|
||||
val signingKey = SecretKeySpec(HmacBlockStream.GetHmacKey64(db.hmacKey, DatabaseInputOutputUtils.ULONG_MAX_VALUE), "HmacSHA256")
|
||||
val signingKey = SecretKeySpec(HmacBlockStream.getHmacKey64(hmacKey, ULONG_MAX_VALUE), "HmacSHA256")
|
||||
hmac.init(signingKey)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw DatabaseOutputException(e)
|
||||
@@ -74,9 +74,9 @@ constructor(private val db: DatabaseKDBX, private val header: DatabaseHeaderKDBX
|
||||
throw DatabaseOutputException(e)
|
||||
}
|
||||
|
||||
dos = DigestOutputStream(os, md)
|
||||
dos = DigestOutputStream(outputStream, md)
|
||||
mos = MacOutputStream(dos, hmac)
|
||||
los = LEDataOutputStream(mos)
|
||||
los = LittleEndianDataOutputStream(mos)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
@@ -86,15 +86,15 @@ constructor(private val db: DatabaseKDBX, private val header: DatabaseHeaderKDBX
|
||||
los.writeUInt(DatabaseHeaderKDBX.DBSIG_2.toLong())
|
||||
los.writeUInt(header.version)
|
||||
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, DatabaseInputOutputUtils.uuidToBytes(db.dataCipher))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, LEDataOutputStream.writeIntBuf(DatabaseHeaderKDBX.getFlagFromCompression(db.compressionAlgorithm)))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.dataCipher))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, intTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
|
||||
|
||||
if (header.version < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformSeed, header.transformSeed)
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, LEDataOutputStream.writeLongBuf(db.numberKeyEncryptionRounds))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, longTo8Bytes(databaseKDBX.numberKeyEncryptionRounds))
|
||||
} else {
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.KdfParameters, KdfParameters.serialize(db.kdfParameters!!))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.KdfParameters, KdfParameters.serialize(databaseKDBX.kdfParameters!!))
|
||||
}
|
||||
|
||||
if (header.encryptionIV.isNotEmpty()) {
|
||||
@@ -104,13 +104,13 @@ constructor(private val db: DatabaseKDBX, private val header: DatabaseHeaderKDBX
|
||||
if (header.version < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomstreamKey, header.innerRandomStreamKey)
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes)
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, LEDataOutputStream.writeIntBuf(header.innerRandomStream!!.id))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, intTo4Bytes(header.innerRandomStream!!.id))
|
||||
}
|
||||
|
||||
if (db.containsPublicCustomData()) {
|
||||
if (databaseKDBX.containsPublicCustomData()) {
|
||||
val bos = ByteArrayOutputStream()
|
||||
val los = LEDataOutputStream(bos)
|
||||
VariantDictionary.serialize(db.publicCustomData, los)
|
||||
val los = LittleEndianDataOutputStream(bos)
|
||||
VariantDictionary.serialize(databaseKDBX.publicCustomData, los)
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.PublicCustomData, bos.toByteArray())
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,8 @@ package com.kunzisoft.keepass.database.file.output
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.stream.ReadBytes
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.readFromStream
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.readBytes
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import kotlin.experimental.or
|
||||
@@ -33,7 +32,7 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
|
||||
private val header: DatabaseHeaderKDBX,
|
||||
outputStream: OutputStream) {
|
||||
|
||||
private val dataOutputStream: LEDataOutputStream = LEDataOutputStream(outputStream)
|
||||
private val dataOutputStream: LittleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream)
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun output() {
|
||||
@@ -58,14 +57,10 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
|
||||
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify
|
||||
dataOutputStream.write(flag.toInt())
|
||||
|
||||
readFromStream(protectedBinary.getInputDataStream(), BUFFER_SIZE_BYTES,
|
||||
object : ReadBytes {
|
||||
override fun read(buffer: ByteArray) {
|
||||
protectedBinary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
dataOutputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader.toInt())
|
||||
dataOutputStream.writeInt(0)
|
||||
|
||||
@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
@@ -128,7 +128,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
header.version = DatabaseHeaderKDB.DBVER_DW
|
||||
header.numGroups = mDatabaseKDB.numberOfGroups()
|
||||
header.numEntries = mDatabaseKDB.numberOfEntries()
|
||||
header.numKeyEncRounds = mDatabaseKDB.numberKeyEncryptionRounds.toInt()
|
||||
header.numKeyEncRounds = mDatabaseKDB.numberKeyEncryptionRounds.toInt() // TODO Signed Long - Unsigned Int
|
||||
|
||||
setIVs(header)
|
||||
|
||||
@@ -197,7 +197,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||
@Throws(DatabaseOutputException::class)
|
||||
fun outputPlanGroupAndEntries(os: OutputStream) {
|
||||
val los = LEDataOutputStream(os)
|
||||
val los = LittleEndianDataOutputStream(os)
|
||||
|
||||
// useHeaderHash
|
||||
if (headerHashBlock != null) {
|
||||
@@ -261,7 +261,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeExtData(headerDigest: ByteArray, os: OutputStream) {
|
||||
val los = LEDataOutputStream(os)
|
||||
val los = LittleEndianDataOutputStream(os)
|
||||
|
||||
writeExtDataField(los, 0x0001, headerDigest, headerDigest.size)
|
||||
val headerRandom = ByteArray(32)
|
||||
@@ -273,7 +273,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeExtDataField(los: LEDataOutputStream, fieldType: Int, data: ByteArray?, fieldSize: Int) {
|
||||
private fun writeExtDataField(los: LittleEndianDataOutputStream, fieldType: Int, data: ByteArray?, fieldSize: Int) {
|
||||
los.writeUShort(fieldType)
|
||||
los.writeInt(fieldSize)
|
||||
if (data != null) {
|
||||
|
||||
@@ -28,7 +28,7 @@ import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||
@@ -47,7 +47,6 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
|
||||
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
import org.joda.time.DateTime
|
||||
import org.spongycastle.crypto.StreamCipher
|
||||
import org.xmlpull.v1.XmlSerializer
|
||||
@@ -95,7 +94,8 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
mOS.write(hashOfHeader!!)
|
||||
mOS.write(headerHmac!!)
|
||||
|
||||
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOS, mDatabaseKDBX.hmacKey))
|
||||
|
||||
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOS, mDatabaseKDBX.hmacKey!!))
|
||||
}
|
||||
|
||||
val osXml: OutputStream
|
||||
@@ -230,7 +230,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
writeUuid(DatabaseKDBXXML.ElemLastTopVisibleGroup, mDatabaseKDBX.lastTopVisibleGroupUUID)
|
||||
|
||||
// Seem to work properly if always in meta
|
||||
// if (header!!.version < DatabaseHeaderKDBX.FILE_VERSION_32_4)
|
||||
if (header!!.version < DatabaseHeaderKDBX.FILE_VERSION_32_4)
|
||||
writeMetaBinaries()
|
||||
|
||||
writeCustomData(mDatabaseKDBX.customData)
|
||||
@@ -390,7 +390,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
} else {
|
||||
val dt = DateTime(value)
|
||||
val seconds = DateKDBXUtil.convertDateToKDBX4Time(dt)
|
||||
val buf = LEDataOutputStream.writeLongBuf(seconds)
|
||||
val buf = longTo8Bytes(seconds)
|
||||
val b64 = String(Base64.encode(buf, BASE_64_FLAG))
|
||||
writeObject(name, b64)
|
||||
}
|
||||
@@ -414,26 +414,29 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeUuid(name: String, uuid: UUID) {
|
||||
val data = DatabaseInputOutputUtils.uuidToBytes(uuid)
|
||||
val data = uuidTo16Bytes(uuid)
|
||||
writeObject(name, String(Base64.encode(data, BASE_64_FLAG)))
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeMetaBinaries() {
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||
private fun writeBinary(binary : BinaryAttachment) {
|
||||
val binaryLength = binary.length()
|
||||
if (binaryLength > 0) {
|
||||
|
||||
mDatabaseKDBX.binaryPool.doForEachBinary { key, binary ->
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrId, key.toString())
|
||||
if (binary.isProtected) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||
|
||||
// Force binary compression from database (compression was harmonized during import)
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed,
|
||||
if (mDatabaseKDBX.compressionAlgorithm === CompressionAlgorithm.GZip) {
|
||||
DatabaseKDBXXML.ValTrue
|
||||
} else {
|
||||
DatabaseKDBXXML.ValFalse
|
||||
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
val encoded = ByteArray(buffer.size)
|
||||
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
|
||||
val charArray = String(Base64.encode(encoded, BASE_64_FLAG)).toCharArray()
|
||||
xml.text(charArray, 0, charArray.size)
|
||||
}
|
||||
} else {
|
||||
// Force binary compression from database (compression was harmonized during import)
|
||||
if (mDatabaseKDBX.compressionAlgorithm === CompressionAlgorithm.GZip) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
||||
}
|
||||
)
|
||||
|
||||
// Force decompression in this specific case
|
||||
val binaryInputStream = if (mDatabaseKDBX.compressionAlgorithm == CompressionAlgorithm.None
|
||||
@@ -444,15 +447,22 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
// Write the XML
|
||||
readFromStream(binaryInputStream, BUFFER_SIZE_BYTES,
|
||||
object : ReadBytes {
|
||||
override fun read(buffer: ByteArray) {
|
||||
binaryInputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
|
||||
xml.text(charArray, 0, charArray.size)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeMetaBinaries() {
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||
|
||||
mDatabaseKDBX.binaryPool.doForEachBinary { key, binary ->
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrId, key.toString())
|
||||
writeBinary(binary)
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
}
|
||||
|
||||
@@ -561,32 +571,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
if (ref != null) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrRef, ref.toString())
|
||||
} else {
|
||||
val binaryLength = binary.length()
|
||||
if (binaryLength > 0) {
|
||||
|
||||
if (binary.isProtected) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||
|
||||
readFromStream(binary.getInputDataStream(), BUFFER_SIZE_BYTES,
|
||||
object : ReadBytes {
|
||||
override fun read(buffer: ByteArray) {
|
||||
val encoded = ByteArray(buffer.size)
|
||||
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
|
||||
val charArray = String(Base64.encode(encoded, BASE_64_FLAG)).toCharArray()
|
||||
xml.text(charArray, 0, charArray.size)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
readFromStream(binary.getInputDataStream(), BUFFER_SIZE_BYTES,
|
||||
object : ReadBytes {
|
||||
override fun read(buffer: ByteArray) {
|
||||
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
|
||||
xml.text(charArray, 0, charArray.size)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
writeBinary(binary)
|
||||
}
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemValue)
|
||||
|
||||
|
||||
@@ -19,12 +19,14 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.file.output
|
||||
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
|
||||
import com.kunzisoft.keepass.database.file.output.GroupOutputKDB.Companion.GROUPID_FIELD_SIZE
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class EntryOutputKDB
|
||||
/**
|
||||
@@ -47,53 +49,76 @@ class EntryOutputKDB
|
||||
// UUID
|
||||
mOutputStream.write(UUID_FIELD_TYPE)
|
||||
mOutputStream.write(UUID_FIELD_SIZE)
|
||||
mOutputStream.write(DatabaseInputOutputUtils.uuidToBytes(mEntry.id))
|
||||
mOutputStream.write(uuidTo16Bytes(mEntry.id))
|
||||
|
||||
// Group ID
|
||||
mOutputStream.write(GROUPID_FIELD_TYPE)
|
||||
mOutputStream.write(LONG_FOUR)
|
||||
mOutputStream.write(LEDataOutputStream.writeIntBuf(mEntry.parent!!.id))
|
||||
mOutputStream.write(GROUPID_FIELD_SIZE)
|
||||
mOutputStream.write(intTo4Bytes(mEntry.parent!!.id))
|
||||
|
||||
// Image ID
|
||||
mOutputStream.write(IMAGEID_FIELD_TYPE)
|
||||
mOutputStream.write(LONG_FOUR)
|
||||
mOutputStream.write(LEDataOutputStream.writeIntBuf(mEntry.icon.iconId))
|
||||
mOutputStream.write(IMAGEID_FIELD_SIZE)
|
||||
mOutputStream.write(intTo4Bytes(mEntry.icon.iconId))
|
||||
|
||||
// Title
|
||||
//byte[] title = mEntry.title.getBytes("UTF-8");
|
||||
mOutputStream.write(TITLE_FIELD_TYPE)
|
||||
length += DatabaseInputOutputUtils.writeCString(mEntry.title, mOutputStream).toLong()
|
||||
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.title, mOutputStream).toLong()
|
||||
|
||||
// URL
|
||||
mOutputStream.write(URL_FIELD_TYPE)
|
||||
length += DatabaseInputOutputUtils.writeCString(mEntry.url, mOutputStream).toLong()
|
||||
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.url, mOutputStream).toLong()
|
||||
|
||||
// Username
|
||||
mOutputStream.write(USERNAME_FIELD_TYPE)
|
||||
length += DatabaseInputOutputUtils.writeCString(mEntry.username, mOutputStream).toLong()
|
||||
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.username, mOutputStream).toLong()
|
||||
|
||||
// Password
|
||||
mOutputStream.write(PASSWORD_FIELD_TYPE)
|
||||
length += DatabaseInputOutputUtils.writePassword(mEntry.password, mOutputStream).toLong()
|
||||
length += writePassword(mEntry.password, mOutputStream).toLong()
|
||||
|
||||
// Additional
|
||||
mOutputStream.write(ADDITIONAL_FIELD_TYPE)
|
||||
length += DatabaseInputOutputUtils.writeCString(mEntry.notes, mOutputStream).toLong()
|
||||
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.notes, mOutputStream).toLong()
|
||||
|
||||
// Create date
|
||||
writeDate(CREATE_FIELD_TYPE, DatabaseInputOutputUtils.writeCDate(mEntry.creationTime.date))
|
||||
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
|
||||
|
||||
// Modification date
|
||||
writeDate(MOD_FIELD_TYPE, DatabaseInputOutputUtils.writeCDate(mEntry.lastModificationTime.date))
|
||||
writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date))
|
||||
|
||||
// Access date
|
||||
writeDate(ACCESS_FIELD_TYPE, DatabaseInputOutputUtils.writeCDate(mEntry.lastAccessTime.date))
|
||||
writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date))
|
||||
|
||||
// Expiration date
|
||||
writeDate(EXPIRE_FIELD_TYPE, DatabaseInputOutputUtils.writeCDate(mEntry.expiryTime.date))
|
||||
writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date))
|
||||
|
||||
// Binary description
|
||||
mOutputStream.write(BINARY_DESC_FIELD_TYPE)
|
||||
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.binaryDescription, mOutputStream).toLong()
|
||||
|
||||
// Binary
|
||||
writeBinary(mEntry.binaryData)
|
||||
mOutputStream.write(BINARY_DATA_FIELD_TYPE)
|
||||
val binaryData = mEntry.binaryData
|
||||
val binaryDataLength = binaryData?.length() ?: 0
|
||||
val binaryDataLengthRightSize = if (binaryDataLength <= Int.MAX_VALUE) {
|
||||
binaryDataLength.toInt()
|
||||
} else {
|
||||
0 // TODO if length > UInt.maxvalue show exception
|
||||
}
|
||||
// Write data length
|
||||
mOutputStream.write(intTo4Bytes(binaryDataLengthRightSize))
|
||||
// Write data
|
||||
if (binaryDataLength > 0) {
|
||||
binaryData?.getInputDataStream().use { inputStream ->
|
||||
inputStream?.readBytes(DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
|
||||
length += buffer.size
|
||||
mOutputStream.write(buffer)
|
||||
}
|
||||
inputStream?.close()
|
||||
}
|
||||
}
|
||||
|
||||
// End
|
||||
mOutputStream.write(END_FIELD_TYPE)
|
||||
@@ -112,39 +137,40 @@ class EntryOutputKDB
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeBinary(data: ByteArray?) {
|
||||
mOutputStream.write(BINARY_DESC_FIELD_TYPE)
|
||||
length += DatabaseInputOutputUtils.writeCString(mEntry.binaryDesc, mOutputStream).toLong()
|
||||
|
||||
val dataLen: Int = data?.size ?: 0
|
||||
mOutputStream.write(BINARY_DATA_FIELD_TYPE)
|
||||
length += DatabaseInputOutputUtils.writeBytes(data, dataLen, mOutputStream)
|
||||
private fun writePassword(str: String, os: OutputStream): Int {
|
||||
val initial = str.toByteArray(Charset.forName("UTF-8"))
|
||||
val length = initial.size + 1
|
||||
os.write(intTo4Bytes(length))
|
||||
os.write(initial)
|
||||
os.write(0x00)
|
||||
return length
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Constants
|
||||
val UUID_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(1)
|
||||
val GROUPID_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(2)
|
||||
val IMAGEID_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(3)
|
||||
val TITLE_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(4)
|
||||
val URL_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(5)
|
||||
val USERNAME_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(6)
|
||||
val PASSWORD_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(7)
|
||||
val ADDITIONAL_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(8)
|
||||
val CREATE_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(9)
|
||||
val MOD_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(10)
|
||||
val ACCESS_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(11)
|
||||
val EXPIRE_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(12)
|
||||
val BINARY_DESC_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(13)
|
||||
val BINARY_DATA_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(14)
|
||||
val END_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(0xFFFF)
|
||||
val LONG_FOUR:ByteArray = LEDataOutputStream.writeIntBuf(4)
|
||||
val UUID_FIELD_SIZE:ByteArray = LEDataOutputStream.writeIntBuf(16)
|
||||
val DATE_FIELD_SIZE:ByteArray = LEDataOutputStream.writeIntBuf(5)
|
||||
val IMAGEID_FIELD_SIZE:ByteArray = LONG_FOUR
|
||||
val LEVEL_FIELD_SIZE:ByteArray = LONG_FOUR
|
||||
val FLAGS_FIELD_SIZE:ByteArray = LONG_FOUR
|
||||
val ZERO_FIELD_SIZE:ByteArray = LEDataOutputStream.writeIntBuf(0)
|
||||
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 = intTo4Bytes(4)
|
||||
val UUID_FIELD_SIZE:ByteArray = intTo4Bytes(16)
|
||||
val DATE_FIELD_SIZE:ByteArray = intTo4Bytes(5)
|
||||
val IMAGEID_FIELD_SIZE:ByteArray = intTo4Bytes(4)
|
||||
val LEVEL_FIELD_SIZE:ByteArray = intTo4Bytes(4)
|
||||
val FLAGS_FIELD_SIZE:ByteArray = intTo4Bytes(4)
|
||||
val ZERO_FIELD_SIZE:ByteArray = intTo4Bytes(0)
|
||||
val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
package com.kunzisoft.keepass.database.file.output
|
||||
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
|
||||
|
||||
import com.kunzisoft.keepass.stream.dateTo5Bytes
|
||||
import com.kunzisoft.keepass.stream.intTo4Bytes
|
||||
import com.kunzisoft.keepass.stream.uShortTo2Bytes
|
||||
import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
@@ -38,46 +39,46 @@ class GroupOutputKDB (private val mGroup: GroupKDB, private val mOutputStream: O
|
||||
// Group ID
|
||||
mOutputStream.write(GROUPID_FIELD_TYPE)
|
||||
mOutputStream.write(GROUPID_FIELD_SIZE)
|
||||
mOutputStream.write(LEDataOutputStream.writeIntBuf(mGroup.id))
|
||||
mOutputStream.write(intTo4Bytes(mGroup.id))
|
||||
|
||||
// Name
|
||||
mOutputStream.write(NAME_FIELD_TYPE)
|
||||
DatabaseInputOutputUtils.writeCString(mGroup.title, mOutputStream)
|
||||
StringDatabaseKDBUtils.writeStringToBytes(mGroup.title, mOutputStream)
|
||||
|
||||
// Create date
|
||||
mOutputStream.write(CREATE_FIELD_TYPE)
|
||||
mOutputStream.write(DATE_FIELD_SIZE)
|
||||
mOutputStream.write(DatabaseInputOutputUtils.writeCDate(mGroup.creationTime.date))
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date))
|
||||
|
||||
// Modification date
|
||||
mOutputStream.write(MOD_FIELD_TYPE)
|
||||
mOutputStream.write(DATE_FIELD_SIZE)
|
||||
mOutputStream.write(DatabaseInputOutputUtils.writeCDate(mGroup.lastModificationTime.date))
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date))
|
||||
|
||||
// Access date
|
||||
mOutputStream.write(ACCESS_FIELD_TYPE)
|
||||
mOutputStream.write(DATE_FIELD_SIZE)
|
||||
mOutputStream.write(DatabaseInputOutputUtils.writeCDate(mGroup.lastAccessTime.date))
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date))
|
||||
|
||||
// Expiration date
|
||||
mOutputStream.write(EXPIRE_FIELD_TYPE)
|
||||
mOutputStream.write(DATE_FIELD_SIZE)
|
||||
mOutputStream.write(DatabaseInputOutputUtils.writeCDate(mGroup.expiryTime.date))
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date))
|
||||
|
||||
// Image ID
|
||||
mOutputStream.write(IMAGEID_FIELD_TYPE)
|
||||
mOutputStream.write(IMAGEID_FIELD_SIZE)
|
||||
mOutputStream.write(LEDataOutputStream.writeIntBuf(mGroup.icon.iconId))
|
||||
mOutputStream.write(intTo4Bytes(mGroup.icon.iconId))
|
||||
|
||||
// Level
|
||||
mOutputStream.write(LEVEL_FIELD_TYPE)
|
||||
mOutputStream.write(LEVEL_FIELD_SIZE)
|
||||
mOutputStream.write(LEDataOutputStream.writeUShortBuf(mGroup.level))
|
||||
mOutputStream.write(uShortTo2Bytes(mGroup.level))
|
||||
|
||||
// Flags
|
||||
mOutputStream.write(FLAGS_FIELD_TYPE)
|
||||
mOutputStream.write(FLAGS_FIELD_SIZE)
|
||||
mOutputStream.write(LEDataOutputStream.writeIntBuf(mGroup.flags))
|
||||
mOutputStream.write(intTo4Bytes(mGroup.flags))
|
||||
|
||||
// End
|
||||
mOutputStream.write(END_FIELD_TYPE)
|
||||
@@ -86,23 +87,22 @@ class GroupOutputKDB (private val mGroup: GroupKDB, private val mOutputStream: O
|
||||
|
||||
companion object {
|
||||
// Constants
|
||||
val GROUPID_FIELD_TYPE: ByteArray = LEDataOutputStream.writeUShortBuf(1)
|
||||
val NAME_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(2)
|
||||
val CREATE_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(3)
|
||||
val MOD_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(4)
|
||||
val ACCESS_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(5)
|
||||
val EXPIRE_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(6)
|
||||
val IMAGEID_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(7)
|
||||
val LEVEL_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(8)
|
||||
val FLAGS_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(9)
|
||||
val END_FIELD_TYPE:ByteArray = LEDataOutputStream.writeUShortBuf(0xFFFF)
|
||||
val LONG_FOUR:ByteArray = LEDataOutputStream.writeIntBuf(4)
|
||||
val GROUPID_FIELD_SIZE:ByteArray = LONG_FOUR
|
||||
val DATE_FIELD_SIZE:ByteArray = LEDataOutputStream.writeIntBuf(5)
|
||||
val IMAGEID_FIELD_SIZE:ByteArray = LONG_FOUR
|
||||
val LEVEL_FIELD_SIZE:ByteArray = LEDataOutputStream.writeIntBuf(2)
|
||||
val FLAGS_FIELD_SIZE:ByteArray = LONG_FOUR
|
||||
val ZERO_FIELD_SIZE:ByteArray = LEDataOutputStream.writeIntBuf(0)
|
||||
}
|
||||
val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1)
|
||||
val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
|
||||
val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
|
||||
val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
|
||||
val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
|
||||
val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
|
||||
val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
|
||||
val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
|
||||
val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
|
||||
val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
|
||||
|
||||
val GROUPID_FIELD_SIZE:ByteArray = intTo4Bytes(4)
|
||||
val DATE_FIELD_SIZE:ByteArray = intTo4Bytes(5)
|
||||
val IMAGEID_FIELD_SIZE:ByteArray = intTo4Bytes(4)
|
||||
val LEVEL_FIELD_SIZE:ByteArray = intTo4Bytes(2)
|
||||
val FLAGS_FIELD_SIZE:ByteArray = intTo4Bytes(4)
|
||||
val ZERO_FIELD_SIZE:ByteArray = intTo4Bytes(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
@@ -19,16 +19,16 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.search;
|
||||
|
||||
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.kunzisoft.keepass.stream.StreamBytesUtilsKt.uuidTo16Bytes;
|
||||
|
||||
public class UuidUtil {
|
||||
|
||||
public static String toHexString(UUID uuid) {
|
||||
if (uuid == null) { return null; }
|
||||
|
||||
byte[] buf = DatabaseInputOutputUtils.INSTANCE.uuidToBytes(uuid);
|
||||
byte[] buf = uuidTo16Bytes(uuid);
|
||||
|
||||
int len = buf.length;
|
||||
if (len == 0) { return ""; }
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.education
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.education
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.education
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.education
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.education
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.education
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.magikeyboard
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
@@ -90,17 +90,10 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
keyboard = Keyboard(this, R.xml.keyboard_password)
|
||||
keyboardEntry = Keyboard(this, R.xml.keyboard_password_entry)
|
||||
|
||||
if (!Database.getInstance().loaded)
|
||||
removeEntryInfo()
|
||||
assignKeyboardView()
|
||||
keyboardView?.setOnKeyboardActionListener(this)
|
||||
keyboardView?.isPreviewEnabled = false
|
||||
|
||||
val context = baseContext
|
||||
val popupFieldsView = LayoutInflater.from(context)
|
||||
.inflate(R.layout.keyboard_popup_fields, FrameLayout(context))
|
||||
|
||||
dismissCustomKeys()
|
||||
popupCustomKeys = PopupWindow(context).apply {
|
||||
width = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
@@ -114,6 +107,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
fieldsAdapter?.onItemClickListener = object : FieldsAdapter.OnItemClickListener {
|
||||
override fun onItemClick(item: Field) {
|
||||
currentInputConnection.commitText(entryInfoKey?.getGeneratedFieldValue(item.name) , 1)
|
||||
goNextAutomatically()
|
||||
}
|
||||
}
|
||||
recyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this, androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, true)
|
||||
@@ -122,6 +116,12 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
|
||||
closeView.setOnClickListener { popupCustomKeys?.dismiss() }
|
||||
|
||||
if (!Database.getInstance().loaded)
|
||||
removeEntryInfo()
|
||||
assignKeyboardView()
|
||||
keyboardView?.setOnKeyboardActionListener(this)
|
||||
keyboardView?.isPreviewEnabled = false
|
||||
|
||||
return rootKeyboardView
|
||||
}
|
||||
|
||||
@@ -235,31 +235,41 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
}
|
||||
KEY_USERNAME -> {
|
||||
if (entryInfoKey != null) {
|
||||
inputConnection.commitText(entryInfoKey!!.username, 1)
|
||||
currentInputConnection.commitText(entryInfoKey!!.username, 1)
|
||||
}
|
||||
goNextAutomatically()
|
||||
}
|
||||
KEY_PASSWORD -> {
|
||||
if (entryInfoKey != null) {
|
||||
inputConnection.commitText(entryInfoKey!!.password, 1)
|
||||
currentInputConnection.commitText(entryInfoKey!!.password, 1)
|
||||
}
|
||||
goNextAutomatically()
|
||||
}
|
||||
KEY_URL -> {
|
||||
if (entryInfoKey != null) {
|
||||
inputConnection.commitText(entryInfoKey!!.url, 1)
|
||||
currentInputConnection.commitText(entryInfoKey!!.url, 1)
|
||||
}
|
||||
goNextAutomatically()
|
||||
}
|
||||
KEY_FIELDS -> {
|
||||
if (entryInfoKey != null) {
|
||||
fieldsAdapter?.fields = entryInfoKey!!.customFields
|
||||
fieldsAdapter?.notifyDataSetChanged()
|
||||
fieldsAdapter?.apply {
|
||||
setFields(entryInfoKey!!.customFields)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
popupCustomKeys?.showAtLocation(keyboardView, Gravity.END or Gravity.TOP, 0, 0)
|
||||
}
|
||||
Keyboard.KEYCODE_DELETE -> inputConnection.deleteSurroundingText(1, 0)
|
||||
Keyboard.KEYCODE_DONE -> inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))
|
||||
Keyboard.KEYCODE_DONE -> inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
|
||||
}// TODO Unlock key
|
||||
}
|
||||
|
||||
private fun goNextAutomatically() {
|
||||
if (PreferencesUtil.isAutoGoActionEnable(this))
|
||||
currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
|
||||
}
|
||||
|
||||
override fun onPress(primaryCode: Int) {
|
||||
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager?
|
||||
if (audioManager != null)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.utils.readEnum
|
||||
import com.kunzisoft.keepass.utils.writeEnum
|
||||
|
||||
data class EntryAttachment(var name: String,
|
||||
var binaryAttachment: BinaryAttachment,
|
||||
var downloadState: AttachmentState = AttachmentState.NULL,
|
||||
var downloadProgression: Int = 0) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString() ?: "",
|
||||
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment(),
|
||||
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
|
||||
parcel.readInt())
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(name)
|
||||
parcel.writeParcelable(binaryAttachment, flags)
|
||||
parcel.writeEnum(downloadState)
|
||||
parcel.writeInt(downloadProgression)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<EntryAttachment> {
|
||||
override fun createFromParcel(parcel: Parcel): EntryAttachment {
|
||||
return EntryAttachment(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<EntryAttachment?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class AttachmentState {
|
||||
NULL, START, IN_PROGRESS, COMPLETE, ERROR
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import android.os.Parcel
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import android.os.Parcel
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import android.os.Parcel
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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.notifications
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileAsyncTask
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
|
||||
class AttachmentFileNotificationService: LockNotificationService() {
|
||||
|
||||
override val notificationId: Int = 10000
|
||||
|
||||
private var mActionTaskBinder = ActionTaskBinder()
|
||||
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
||||
|
||||
inner class ActionTaskBinder: Binder() {
|
||||
|
||||
fun getService(): AttachmentFileNotificationService = this@AttachmentFileNotificationService
|
||||
|
||||
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||
mActionTaskListeners.add(actionTaskListener)
|
||||
|
||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
||||
entry.value.attachmentTask?.onUpdate = { uri, attachment, notificationIdAttach ->
|
||||
newNotification(uri, attachment, notificationIdAttach)
|
||||
mActionTaskListeners.forEach { actionListener ->
|
||||
actionListener.onAttachmentProgress(entry.key, attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
||||
entry.value.attachmentTask?.onUpdate = null
|
||||
}
|
||||
})
|
||||
|
||||
mActionTaskListeners.remove(actionTaskListener)
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionTaskListener {
|
||||
fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return mActionTaskBinder
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
||||
val downloadFileUri: Uri? = if (intent?.hasExtra(DOWNLOAD_FILE_URI_KEY) == true) {
|
||||
intent.getParcelableExtra(DOWNLOAD_FILE_URI_KEY)
|
||||
} else null
|
||||
|
||||
when(intent?.action) {
|
||||
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
|
||||
if (downloadFileUri != null
|
||||
&& intent.hasExtra(ATTACHMENT_KEY)) {
|
||||
|
||||
val nextNotificationId = (downloadFileUris.values.maxBy { it.notificationId }
|
||||
?.notificationId ?: notificationId) + 1
|
||||
|
||||
val entryAttachment: EntryAttachment = intent.getParcelableExtra(ATTACHMENT_KEY)
|
||||
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
|
||||
downloadFileUris[downloadFileUri] = attachmentNotification
|
||||
try {
|
||||
AttachmentFileAsyncTask(downloadFileUri,
|
||||
attachmentNotification,
|
||||
contentResolver).apply {
|
||||
onUpdate = { uri, attachment, notificationIdAttach ->
|
||||
newNotification(uri, attachment, notificationIdAttach)
|
||||
mActionTaskListeners.forEach { actionListener ->
|
||||
actionListener.onAttachmentProgress(downloadFileUri, attachment)
|
||||
}
|
||||
}
|
||||
}.execute()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to download $downloadFileUri", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (downloadFileUri != null) {
|
||||
downloadFileUris[downloadFileUri]?.notificationId?.let {
|
||||
notificationManager?.cancel(it)
|
||||
downloadFileUris.remove(downloadFileUri)
|
||||
}
|
||||
}
|
||||
if (downloadFileUris.isEmpty()) {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
fun checkCurrentAttachmentProgress() {
|
||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
||||
mActionTaskListeners.forEach { actionListener ->
|
||||
actionListener.onAttachmentProgress(entry.key, entry.value.entryAttachment)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun newNotification(downloadFileUri: Uri,
|
||||
entryAttachment: EntryAttachment,
|
||||
notificationIdAttachment: Int) {
|
||||
|
||||
val pendingContentIntent = PendingIntent.getActivity(this,
|
||||
0,
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(downloadFileUri, contentResolver.getType(downloadFileUri))
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
|
||||
val pendingDeleteIntent = PendingIntent.getService(this,
|
||||
0,
|
||||
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
||||
// No action to delete the service
|
||||
putExtra(DOWNLOAD_FILE_URI_KEY, downloadFileUri)
|
||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
|
||||
val fileName = DocumentFile.fromSingleUri(this, downloadFileUri)?.name ?: ""
|
||||
|
||||
val builder = buildNewNotification().apply {
|
||||
setSmallIcon(R.drawable.ic_file_download_white_24dp)
|
||||
setContentTitle(getString(R.string.download_attachment, fileName))
|
||||
setAutoCancel(false)
|
||||
when (entryAttachment.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.START -> {
|
||||
setContentText(getString(R.string.download_initialization))
|
||||
setOngoing(true)
|
||||
}
|
||||
AttachmentState.IN_PROGRESS -> {
|
||||
if (entryAttachment.downloadProgression > 100) {
|
||||
setContentText(getString(R.string.download_finalization))
|
||||
} else {
|
||||
setProgress(100, entryAttachment.downloadProgression, false)
|
||||
setContentText(getString(R.string.download_progression, entryAttachment.downloadProgression))
|
||||
}
|
||||
setOngoing(true)
|
||||
}
|
||||
AttachmentState.COMPLETE, AttachmentState.ERROR -> {
|
||||
setContentText(getString(R.string.download_complete))
|
||||
setContentIntent(pendingContentIntent)
|
||||
setDeleteIntent(pendingDeleteIntent)
|
||||
setOngoing(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
notificationManager?.notify(notificationIdAttachment, builder.build())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
||||
entry.value.attachmentTask?.onUpdate = null
|
||||
notificationManager?.cancel(entry.value.notificationId)
|
||||
}
|
||||
})
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
data class AttachmentNotification(var notificationId: Int,
|
||||
var entryAttachment: EntryAttachment,
|
||||
var attachmentTask: AttachmentFileAsyncTask? = null) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as AttachmentNotification
|
||||
|
||||
if (notificationId != other.notificationId) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return notificationId
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = AttachmentFileNotificationService::javaClass.name
|
||||
|
||||
const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
|
||||
|
||||
const val DOWNLOAD_FILE_URI_KEY = "DOWNLOAD_FILE_URI_KEY"
|
||||
const val ATTACHMENT_KEY = "ATTACHMENT_KEY"
|
||||
|
||||
private val downloadFileUris = HashMap<Uri, AttachmentNotification>()
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user