Merge branch 'feature/Refactor_Kotlin' of github.com:Kunzisoft/KeePassDX into feature/Refactor_Kotlin

This commit is contained in:
J-Jamet
2019-07-14 17:03:44 +02:00
200 changed files with 11521 additions and 12008 deletions

View File

@@ -1,10 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 27
buildToolsVersion '28.0.2'
buildToolsVersion '28.0.3'
defaultConfig {
applicationId "com.kunzisoft.keepass"
@@ -80,7 +81,7 @@ android {
def supportVersion = "27.1.1"
def spongycastleVersion = "1.58.0.0"
def permissionDispatcherVersion = "3.1.0"
def permissionDispatcherVersion = "3.3.1"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@@ -103,7 +104,7 @@ dependencies {
// if you don't use android.app.Fragment you can exclude support for them
exclude module: "support-v13"
}
annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion"
kapt "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion"
// Apache Commons Collections
implementation 'commons-collections:commons-collections:3.2.1'
implementation 'org.apache.commons:commons-io:1.3.2'

View File

@@ -1,22 +1,22 @@
/*
* 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/>.
*
*/
* 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.tests;
import android.test.AndroidTestCase;
@@ -25,18 +25,20 @@ import com.kunzisoft.keepass.tests.database.TestData;
public class AccentTest extends AndroidTestCase {
private static final String KEYFILE = "";
private static final String PASSWORD = "é";
private static final String ASSET = "accent.kdb";
private static final String FILENAME = "/sdcard/accent.kdb";
private static final String KEYFILE = "";
private static final String PASSWORD = "é";
private static final String ASSET = "accent.kdb";
private static final String FILENAME = "/sdcard/accent.kdb";
public void testOpen() {
public void testOpen() {
try {
TestData.GetDb(getContext(), ASSET, PASSWORD, KEYFILE, FILENAME);
} catch (Exception e) {
assertTrue("Failed to open database", false);
}
}
/*
try {
TestData.GetDb(getContext(), ASSET, PASSWORD, KEYFILE, FILENAME);
} catch (Exception e) {
assertTrue("Failed to open database", false);
}
*/
}
}

View File

@@ -26,10 +26,10 @@ import android.test.suitebuilder.TestSuiteBuilder;
public class OutputTests extends TestSuite {
public static Test suite() {
public static Test suite() {
return new TestSuiteBuilder(AllTests.class)
.includePackages("com.kunzisoft.keepass.tests.output")
.build();
}
return new TestSuiteBuilder(AllTests.class)
.includePackages("com.kunzisoft.keepass.tests.output")
.build();
}
}

View File

@@ -17,22 +17,21 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.tests;
package com.kunzisoft.keepass.tests
import junit.framework.TestCase;
import junit.framework.TestCase
import com.kunzisoft.keepass.database.element.PwDate;
import com.kunzisoft.keepass.database.element.PwDate
import org.junit.Assert
public class PwDateTest extends TestCase {
public void testDate() {
PwDate jDate = new PwDate(System.currentTimeMillis());
class PwDateTest : TestCase() {
PwDate intermediate = new PwDate(jDate);
fun testDate() {
val jDate = PwDate(System.currentTimeMillis())
val intermediate = PwDate(jDate)
val cDate = PwDate(intermediate.byteArrayDate!!, 0)
PwDate cDate = new PwDate(intermediate.getCDate(), 0);
assertTrue("jDate and intermediate not equal", jDate.equals(intermediate));
assertTrue("jDate and cDate not equal", cDate.equals(jDate));
}
Assert.assertTrue("jDate and intermediate not equal", jDate == intermediate)
Assert.assertTrue("jDate and cDate not equal", cDate == jDate)
}
}

View File

@@ -31,33 +31,33 @@ import com.kunzisoft.keepass.database.element.PwEntryV3;
import com.kunzisoft.keepass.tests.database.TestData;
public class PwEntryTestV3 extends AndroidTestCase {
PwEntryV3 mPE;
PwEntryV3 mPE;
@Override
protected void setUp() throws Exception {
super.setUp();
@Override
protected void setUp() throws Exception {
super.setUp();
// mPE = (PwEntryV3) TestData.GetTest1(getContext()).getEntryAt(0);
// mPE = (PwEntryV3) TestData.GetTest1(getContext()).getEntryAt(0);
}
}
public void testName() {
assertTrue("Name was " + mPE.getTitle(), mPE.getTitle().equals("Amazon"));
}
public void testName() {
assertTrue("Name was " + mPE.getTitle(), mPE.getTitle().equals("Amazon"));
}
public void testPassword() throws UnsupportedEncodingException {
String sPass = "12345";
byte[] password = sPass.getBytes("UTF-8");
public void testPassword() throws UnsupportedEncodingException {
String sPass = "12345";
byte[] password = sPass.getBytes("UTF-8");
assertArrayEquals(password, mPE.getPasswordBytes());
}
assertArrayEquals(password, mPE.getPasswordBytes());
}
public void testCreation() {
Calendar cal = Calendar.getInstance();
cal.setTime(mPE.getCreationTime().getDate());
public void testCreation() {
Calendar cal = Calendar.getInstance();
cal.setTime(mPE.getCreationTime().getDate());
assertEquals("Incorrect year.", cal.get(Calendar.YEAR), 2009);
assertEquals("Incorrect month.", cal.get(Calendar.MONTH), 3);
assertEquals("Incorrect day.", cal.get(Calendar.DAY_OF_MONTH), 23);
}
assertEquals("Incorrect year.", cal.get(Calendar.YEAR), 2009);
assertEquals("Incorrect month.", cal.get(Calendar.MONTH), 3);
assertEquals("Incorrect day.", cal.get(Calendar.DAY_OF_MONTH), 23);
}
}

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.tests;
import junit.framework.TestCase;
public class PwEntryTestV4 extends TestCase {
public void testAssign() {
public void testAssign() {
/*
TODO Test
PwEntryV4 entry = new PwEntryV4();
@@ -54,6 +54,6 @@ public class PwEntryTestV4 extends TestCase {
assertTrue("Entries do not match.", entry.equals(target));
*/
}
}
}

View File

@@ -27,18 +27,18 @@ import com.kunzisoft.keepass.tests.database.TestData;
public class PwGroupTest extends AndroidTestCase {
PwGroupV3 mPG;
PwGroupV3 mPG;
@Override
protected void setUp() throws Exception {
super.setUp();
@Override
protected void setUp() throws Exception {
super.setUp();
//mPG = (PwGroupV3) TestData.GetTest1(getContext()).getGroups().get(0);
//mPG = (PwGroupV3) TestData.GetTest1(getContext()).getGroups().get(0);
}
}
public void testGroupName() {
//assertTrue("Name was " + mPG.getTitle(), mPG.getTitle().equals("Internet"));
}
public void testGroupName() {
//assertTrue("Name was " + mPG.getTitle(), mPG.getTitle().equals("Internet"));
}
}

View File

@@ -33,29 +33,29 @@ import com.kunzisoft.keepass.utils.EmptyUtils;
import com.kunzisoft.keepass.utils.UriUtil;
public class TestUtil {
private static final File sdcard = Environment.getExternalStorageDirectory();
private static final File sdcard = Environment.getExternalStorageDirectory();
public static void extractKey(Context ctx, String asset, String target) throws Exception {
public static void extractKey(Context ctx, String asset, String target) throws Exception {
InputStream key = ctx.getAssets().open(asset, AssetManager.ACCESS_STREAMING);
InputStream key = ctx.getAssets().open(asset, AssetManager.ACCESS_STREAMING);
FileOutputStream keyFile = new FileOutputStream(target);
while (true) {
byte[] buf = new byte[1024];
int read = key.read(buf);
if ( read == -1 ) {
break;
} else {
keyFile.write(buf, 0, read);
}
}
FileOutputStream keyFile = new FileOutputStream(target);
while (true) {
byte[] buf = new byte[1024];
int read = key.read(buf);
if ( read == -1 ) {
break;
} else {
keyFile.write(buf, 0, read);
}
}
keyFile.close();
keyFile.close();
}
}
public static String getSdPath(String filename) {
File file = new File(sdcard, filename);
return file.getAbsolutePath();
}
public static String getSdPath(String filename) {
File file = new File(sdcard, filename);
return file.getAbsolutePath();
}
}

View File

@@ -35,177 +35,177 @@ import com.kunzisoft.keepass.utils.Types;
public class TypesTest extends TestCase {
public void testReadWriteLongZero() {
testReadWriteLong((byte) 0);
}
public void testReadWriteLongZero() {
testReadWriteLong((byte) 0);
}
public void testReadWriteLongMax() {
testReadWriteLong(Byte.MAX_VALUE);
}
public void testReadWriteLongMax() {
testReadWriteLong(Byte.MAX_VALUE);
}
public void testReadWriteLongMin() {
testReadWriteLong(Byte.MIN_VALUE);
}
public void testReadWriteLongMin() {
testReadWriteLong(Byte.MIN_VALUE);
}
public void testReadWriteLongRnd() {
Random rnd = new Random();
byte[] buf = new byte[1];
rnd.nextBytes(buf);
public void testReadWriteLongRnd() {
Random rnd = new Random();
byte[] buf = new byte[1];
rnd.nextBytes(buf);
testReadWriteLong(buf[0]);
}
testReadWriteLong(buf[0]);
}
private void testReadWriteLong(byte value) {
byte[] orig = new byte[8];
byte[] dest = new byte[8];
private void testReadWriteLong(byte value) {
byte[] orig = new byte[8];
byte[] dest = new byte[8];
setArray(orig, value, 0, 8);
setArray(orig, value, 0, 8);
long one = LEDataInputStream.readLong(orig, 0);
LEDataOutputStream.writeLong(one, dest, 0);
long one = LEDataInputStream.readLong(orig, 0);
LEDataOutputStream.writeLong(one, dest, 0);
assertArrayEquals(orig, dest);
assertArrayEquals(orig, dest);
}
}
public void testReadWriteIntZero() {
testReadWriteInt((byte) 0);
}
public void testReadWriteIntZero() {
testReadWriteInt((byte) 0);
}
public void testReadWriteIntMin() {
testReadWriteInt(Byte.MIN_VALUE);
}
public void testReadWriteIntMin() {
testReadWriteInt(Byte.MIN_VALUE);
}
public void testReadWriteIntMax() {
testReadWriteInt(Byte.MAX_VALUE);
}
public void testReadWriteIntMax() {
testReadWriteInt(Byte.MAX_VALUE);
}
private void testReadWriteInt(byte value) {
byte[] orig = new byte[4];
byte[] dest = new byte[4];
private void testReadWriteInt(byte value) {
byte[] orig = new byte[4];
byte[] dest = new byte[4];
for (int i = 0; i < 4; i++ ) {
orig[i] = 0;
}
for (int i = 0; i < 4; i++ ) {
orig[i] = 0;
}
setArray(orig, value, 0, 4);
setArray(orig, value, 0, 4);
int one = LEDataInputStream.readInt(orig, 0);
int one = LEDataInputStream.readInt(orig, 0);
LEDataOutputStream.writeInt(one, dest, 0);
LEDataOutputStream.writeInt(one, dest, 0);
assertArrayEquals(orig, dest);
assertArrayEquals(orig, dest);
}
}
private void setArray(byte[] buf, byte value, int offset, int size) {
for (int i = offset; i < offset + size; i++) {
buf[i] = value;
}
}
private void setArray(byte[] buf, byte value, int offset, int size) {
for (int i = offset; i < offset + size; i++) {
buf[i] = value;
}
}
public void testReadWriteShortOne() {
byte[] orig = new byte[2];
byte[] dest = new byte[2];
public void testReadWriteShortOne() {
byte[] orig = new byte[2];
byte[] dest = new byte[2];
orig[0] = 0;
orig[1] = 1;
orig[0] = 0;
orig[1] = 1;
int one = LEDataInputStream.readUShort(orig, 0);
dest = LEDataOutputStream.writeUShortBuf(one);
int one = LEDataInputStream.readUShort(orig, 0);
dest = LEDataOutputStream.writeUShortBuf(one);
assertArrayEquals(orig, dest);
assertArrayEquals(orig, dest);
}
}
public void testReadWriteShortMin() {
testReadWriteShort(Byte.MIN_VALUE);
}
public void testReadWriteShortMin() {
testReadWriteShort(Byte.MIN_VALUE);
}
public void testReadWriteShortMax() {
testReadWriteShort(Byte.MAX_VALUE);
}
public void testReadWriteShortMax() {
testReadWriteShort(Byte.MAX_VALUE);
}
private void testReadWriteShort(byte value) {
byte[] orig = new byte[2];
byte[] dest = new byte[2];
private void testReadWriteShort(byte value) {
byte[] orig = new byte[2];
byte[] dest = new byte[2];
setArray(orig, value, 0, 2);
setArray(orig, value, 0, 2);
int one = LEDataInputStream.readUShort(orig, 0);
LEDataOutputStream.writeUShort(one, dest, 0);
int one = LEDataInputStream.readUShort(orig, 0);
LEDataOutputStream.writeUShort(one, dest, 0);
assertArrayEquals(orig, dest);
assertArrayEquals(orig, dest);
}
}
public void testReadWriteByteZero() {
testReadWriteByte((byte) 0);
}
public void testReadWriteByteZero() {
testReadWriteByte((byte) 0);
}
public void testReadWriteByteMin() {
testReadWriteByte(Byte.MIN_VALUE);
}
public void testReadWriteByteMin() {
testReadWriteByte(Byte.MIN_VALUE);
}
public void testReadWriteByteMax() {
testReadWriteShort(Byte.MAX_VALUE);
}
public void testReadWriteByteMax() {
testReadWriteShort(Byte.MAX_VALUE);
}
private void testReadWriteByte(byte value) {
byte[] orig = new byte[1];
byte[] dest = new byte[1];
private void testReadWriteByte(byte value) {
byte[] orig = new byte[1];
byte[] dest = new byte[1];
setArray(orig, value, 0, 1);
setArray(orig, value, 0, 1);
int one = Types.readUByte(orig, 0);
Types.writeUByte(one, dest, 0);
int one = Types.readUByte(orig, 0);
Types.writeUByte(one, dest, 0);
assertArrayEquals(orig, dest);
assertArrayEquals(orig, dest);
}
}
public void testDate() {
Calendar cal = Calendar.getInstance();
public void testDate() {
Calendar cal = Calendar.getInstance();
Calendar expected = Calendar.getInstance();
expected.set(2008, 1, 2, 3, 4, 5);
Calendar expected = Calendar.getInstance();
expected.set(2008, 1, 2, 3, 4, 5);
byte[] buf = PwDate.writeTime(expected.getTime(), cal);
Calendar actual = Calendar.getInstance();
actual.setTime(PwDate.readTime(buf, 0, cal));
byte[] buf = PwDate.Companion.writeTime(expected.getTime(), cal);
Calendar actual = Calendar.getInstance();
actual.setTime(PwDate.Companion.readTime(buf, 0, cal));
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR));
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH));
assertEquals("Day mismatch: ", 1, 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));
}
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR));
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH));
assertEquals("Day mismatch: ", 1, 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));
}
public void testUUID() {
Random rnd = new Random();
byte[] bUUID = new byte[16];
rnd.nextBytes(bUUID);
public void testUUID() {
Random rnd = new Random();
byte[] bUUID = new byte[16];
rnd.nextBytes(bUUID);
UUID uuid = Types.bytestoUUID(bUUID);
byte[] eUUID = Types.UUIDtoBytes(uuid);
UUID uuid = Types.bytestoUUID(bUUID);
byte[] eUUID = Types.UUIDtoBytes(uuid);
assertArrayEquals("UUID match failed", bUUID, eUUID);
}
assertArrayEquals("UUID match failed", bUUID, eUUID);
}
public void testULongMax() throws Exception {
byte[] ulongBytes = new byte[8];
for (int i = 0; i < ulongBytes.length; i++) {
ulongBytes[i] = -1;
}
public void testULongMax() throws Exception {
byte[] ulongBytes = new byte[8];
for (int i = 0; i < ulongBytes.length; i++) {
ulongBytes[i] = -1;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
LEDataOutputStream leos = new LEDataOutputStream(bos);
leos.writeLong(Types.ULONG_MAX_VALUE);
leos.close();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
LEDataOutputStream leos = new LEDataOutputStream(bos);
leos.writeLong(Types.ULONG_MAX_VALUE);
leos.close();
byte[] uLongMax = bos.toByteArray();
byte[] uLongMax = bos.toByteArray();
assertArrayEquals(ulongBytes, uLongMax);
}
assertArrayEquals(ulongBytes, uLongMax);
}
}

View File

@@ -1,22 +1,22 @@
/*
* 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/>.
*
*/
* 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.tests.crypto;
import com.kunzisoft.keepass.crypto.CipherFactory;
@@ -39,45 +39,45 @@ import static org.junit.Assert.assertArrayEquals;
public class AESTest extends TestCase {
private Random mRand = new Random();
private Random mRand = new Random();
public void testEncrypt() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
// Test above below and at the blocksize
testFinal(15);
testFinal(16);
testFinal(17);
public void testEncrypt() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
// Test above below and at the blocksize
testFinal(15);
testFinal(16);
testFinal(17);
// Test random larger sizes
int size = mRand.nextInt(494) + 18;
testFinal(size);
}
// Test random larger sizes
int size = mRand.nextInt(494) + 18;
testFinal(size);
}
private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
// Generate some input
byte[] input = new byte[dataSize];
mRand.nextBytes(input);
// Generate some input
byte[] input = new byte[dataSize];
mRand.nextBytes(input);
// Generate key
byte[] keyArray = new byte[32];
mRand.nextBytes(keyArray);
SecretKeySpec key = new SecretKeySpec(keyArray, "AES");
// Generate key
byte[] keyArray = new byte[32];
mRand.nextBytes(keyArray);
SecretKeySpec key = new SecretKeySpec(keyArray, "AES");
// Generate IV
byte[] ivArray = new byte[16];
mRand.nextBytes(ivArray);
IvParameterSpec iv = new IvParameterSpec(ivArray);
// Generate IV
byte[] ivArray = new byte[16];
mRand.nextBytes(ivArray);
IvParameterSpec iv = new IvParameterSpec(ivArray);
Cipher android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true);
android.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outAndroid = android.doFinal(input, 0, dataSize);
Cipher android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true);
android.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outAndroid = android.doFinal(input, 0, dataSize);
Cipher nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding");
nat.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outNative = nat.doFinal(input, 0, dataSize);
Cipher nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding");
nat.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outNative = nat.doFinal(input, 0, dataSize);
assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative);
}
assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative);
}
}

View File

@@ -1,22 +1,22 @@
/*
* 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/>.
*
*/
* 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.tests.crypto;
import static org.junit.Assert.assertArrayEquals;
@@ -44,57 +44,57 @@ import com.kunzisoft.keepass.stream.BetterCipherInputStream;
import com.kunzisoft.keepass.stream.LEDataInputStream;
public class CipherTest extends TestCase {
private Random rand = new Random();
private Random rand = new Random();
public void testCipherFactory() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
byte[] key = new byte[32];
byte[] iv = new byte[16];
public void testCipherFactory() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
byte[] key = new byte[32];
byte[] iv = new byte[16];
byte[] plaintext = new byte[1024];
byte[] plaintext = new byte[1024];
rand.nextBytes(key);
rand.nextBytes(iv);
rand.nextBytes(plaintext);
rand.nextBytes(key);
rand.nextBytes(iv);
rand.nextBytes(plaintext);
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
byte[] secrettext = encrypt.doFinal(plaintext);
byte[] decrypttext = decrypt.doFinal(secrettext);
byte[] secrettext = encrypt.doFinal(plaintext);
byte[] decrypttext = decrypt.doFinal(secrettext);
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
}
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
}
public void testCipherStreams() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException {
final int MESSAGE_LENGTH = 1024;
public void testCipherStreams() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException {
final int MESSAGE_LENGTH = 1024;
byte[] key = new byte[32];
byte[] iv = new byte[16];
byte[] key = new byte[32];
byte[] iv = new byte[16];
byte[] plaintext = new byte[MESSAGE_LENGTH];
byte[] plaintext = new byte[MESSAGE_LENGTH];
rand.nextBytes(key);
rand.nextBytes(iv);
rand.nextBytes(plaintext);
rand.nextBytes(key);
rand.nextBytes(iv);
rand.nextBytes(plaintext);
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
CipherOutputStream cos = new CipherOutputStream(bos, encrypt);
cos.write(plaintext);
cos.close();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
CipherOutputStream cos = new CipherOutputStream(bos, encrypt);
cos.write(plaintext);
cos.close();
byte[] secrettext = bos.toByteArray();
byte[] secrettext = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(secrettext);
BetterCipherInputStream cis = new BetterCipherInputStream(bis, decrypt);
LEDataInputStream lis = new LEDataInputStream(cis);
ByteArrayInputStream bis = new ByteArrayInputStream(secrettext);
BetterCipherInputStream cis = new BetterCipherInputStream(bis, decrypt);
LEDataInputStream lis = new LEDataInputStream(cis);
byte[] decrypttext = lis.readBytes(MESSAGE_LENGTH);
byte[] decrypttext = lis.readBytes(MESSAGE_LENGTH);
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
}
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
}
}

View File

@@ -1,22 +1,22 @@
/*
* 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/>.
*
*/
* 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.tests.crypto;
import static org.junit.Assert.assertArrayEquals;
@@ -30,37 +30,37 @@ import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey;
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey;
public class FinalKeyTest extends TestCase {
private Random mRand;
private Random mRand;
@Override
protected void setUp() throws Exception {
super.setUp();
@Override
protected void setUp() throws Exception {
super.setUp();
mRand = new Random();
}
mRand = new Random();
}
public void testNativeAndroid() throws IOException {
// Test both an old and an even number to test my flip variable
testNativeFinalKey(5);
testNativeFinalKey(6);
}
public void testNativeAndroid() throws IOException {
// Test both an old and an even number to test my flip variable
testNativeFinalKey(5);
testNativeFinalKey(6);
}
private void testNativeFinalKey(int rounds) throws IOException {
byte[] seed = new byte[32];
byte[] key = new byte[32];
byte[] nativeKey;
byte[] androidKey;
private void testNativeFinalKey(int rounds) throws IOException {
byte[] seed = new byte[32];
byte[] key = new byte[32];
byte[] nativeKey;
byte[] androidKey;
mRand.nextBytes(seed);
mRand.nextBytes(key);
mRand.nextBytes(seed);
mRand.nextBytes(key);
AndroidFinalKey aKey = new AndroidFinalKey();
androidKey = aKey.transformMasterKey(seed, key, rounds);
AndroidFinalKey aKey = new AndroidFinalKey();
androidKey = aKey.transformMasterKey(seed, key, rounds);
NativeFinalKey nKey = new NativeFinalKey();
nativeKey = nKey.transformMasterKey(seed, key, rounds);
NativeFinalKey nKey = new NativeFinalKey();
nativeKey = nKey.transformMasterKey(seed, key, rounds);
assertArrayEquals("Does not match", androidKey, nativeKey);
assertArrayEquals("Does not match", androidKey, nativeKey);
}
}
}

View File

@@ -26,15 +26,15 @@ import com.kunzisoft.keepass.database.element.PwDatabaseV3;
import com.kunzisoft.keepass.database.element.PwEntryV3;
public class DeleteEntry extends AndroidTestCase {
private static final String GROUP1_NAME = "Group1";
private static final String ENTRY1_NAME = "Test1";
private static final String ENTRY2_NAME = "Test2";
private static final String KEYFILE = "";
private static final String PASSWORD = "12345";
private static final String ASSET = "delete.kdb";
private static final String FILENAME = "/sdcard/delete.kdb";
private static final String GROUP1_NAME = "Group1";
private static final String ENTRY1_NAME = "Test1";
private static final String ENTRY2_NAME = "Test2";
private static final String KEYFILE = "";
private static final String PASSWORD = "12345";
private static final String ASSET = "delete.kdb";
private static final String FILENAME = "/sdcard/delete.kdb";
public void testDelete() {
public void testDelete() {
/*
Database db;
@@ -76,9 +76,9 @@ public class DeleteEntry extends AndroidTestCase {
assertNull("Group 1 was not removed.", group1);
*/
}
}
private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) {
private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) {
/*
TODO test
List<PwEntryV3> entries = pm.getEntries();
@@ -89,11 +89,11 @@ public class DeleteEntry extends AndroidTestCase {
}
}
*/
return null;
return null;
}
}
private GroupVersioned getGroup(PwDatabase pm, String name) {
private GroupVersioned getGroup(PwDatabase pm, String name) {
/*
List<GroupVersioned> groups = pm.getGroups();
for ( int i = 0; i < groups.size(); i++ ) {
@@ -104,8 +104,8 @@ public class DeleteEntry extends AndroidTestCase {
}
*/
return null;
}
return null;
}
}

View File

@@ -26,7 +26,7 @@ import junit.framework.TestCase;
public class EntryV4 extends TestCase {
public void testBackup() {
public void testBackup() {
/*
PwDatabaseV4 db = new PwDatabaseV4();
@@ -51,6 +51,6 @@ public class EntryV4 extends TestCase {
assertEquals("Title2", backup.getTitle());
assertEquals("User2", backup.getUsername());
*/
}
}
}

View File

@@ -23,7 +23,7 @@ import android.test.AndroidTestCase;
public class Kdb3 extends AndroidTestCase {
private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception {
private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception {
/*
Context ctx = getContext();
@@ -40,14 +40,14 @@ public class Kdb3 extends AndroidTestCase {
is.close();
*/
}
}
public void testXMLKeyFile() throws Exception {
testKeyfile("kdb_with_xml_keyfile.kdb", "keyfile.key", "12345");
}
public void testXMLKeyFile() throws Exception {
testKeyfile("kdb_with_xml_keyfile.kdb", "keyfile.key", "12345");
}
public void testBinary64KeyFile() throws Exception {
testKeyfile("binary-key.kdb", "binary.key", "12345");
}
public void testBinary64KeyFile() throws Exception {
testKeyfile("binary-key.kdb", "binary.key", "12345");
}
}

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.tests.database;
import android.test.AndroidTestCase;
public class Kdb3Twofish extends AndroidTestCase {
public void testReadTwofish() throws Exception {
public void testReadTwofish() throws Exception {
/*
Context ctx = getContext();
@@ -37,5 +37,5 @@ public class Kdb3Twofish extends AndroidTestCase {
is.close();
*/
}
}
}

View File

@@ -26,11 +26,11 @@ import android.test.AndroidTestCase;
import java.io.InputStream;
public class Kdb4Header extends AndroidTestCase {
public void testReadHeader() throws Exception {
Context ctx = getContext();
public void testReadHeader() throws Exception {
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
@@ -45,5 +45,5 @@ public class Kdb4Header extends AndroidTestCase {
is.close();
*/
}
}
}

View File

@@ -35,17 +35,17 @@ import java.util.UUID;
import biz.source_code.base64Coder.Base64Coder;
public class SprEngineTest extends AndroidTestCase {
private PwDatabaseV4 db;
private SprEngineV4 spr;
private PwDatabaseV4 db;
private SprEngineV4 spr;
@Override
protected void setUp() throws Exception {
super.setUp();
@Override
protected void setUp() throws Exception {
super.setUp();
Context ctx = getContext();
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
@@ -56,12 +56,12 @@ public class SprEngineTest extends AndroidTestCase {
spr = new SprEngineV4();
*/
}
}
private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}";
private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw==";
private final String RESULT = "Password";
public void testRefReplace() {
private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}";
private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw==";
private final String RESULT = "Password";
public void testRefReplace() {
/*
TODO TEST
UUID entryUUID = decodeUUID(ENCODE_UUID);
@@ -72,15 +72,15 @@ public class SprEngineTest extends AndroidTestCase {
assertEquals(RESULT, spr.compile(REF, entry, db));
*/
}
}
private UUID decodeUUID(String encoded) {
if (encoded == null || encoded.length() == 0 ) {
return PwDatabase.UUID_ZERO;
}
private UUID decodeUUID(String encoded) {
if (encoded == null || encoded.length() == 0 ) {
return PwDatabase.UUID_ZERO;
}
byte[] buf = Base64Coder.decode(encoded);
return Types.bytestoUUID(buf);
}
byte[] buf = Base64Coder.decode(encoded);
return Types.bytestoUUID(buf);
}
}

View File

@@ -22,11 +22,11 @@ package com.kunzisoft.keepass.tests.database;
import com.kunzisoft.keepass.database.element.Database;
public class TestData {
private static final String TEST1_KEYFILE = "";
private static final String TEST1_KDB = "test1.kdb";
private static final String TEST1_PASSWORD = "12345";
private static final String TEST1_KEYFILE = "";
private static final String TEST1_KDB = "test1.kdb";
private static final String TEST1_PASSWORD = "12345";
private static Database mDb1;
private static Database mDb1;
/*

View File

@@ -1,22 +1,22 @@
/*
* 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/>.
*
*/
* 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.tests.search;
@@ -29,42 +29,42 @@ import com.kunzisoft.keepass.database.element.GroupVersioned;
public class SearchTest extends AndroidTestCase {
private Database mDb;
private Database mDb;
@Override
protected void setUp() throws Exception {
super.setUp();
@Override
protected void setUp() throws Exception {
super.setUp();
//mDb = TestData.GetDb1(getContext(), true);
}
//mDb = TestData.GetDb1(getContext(), true);
}
public void testSearch() {
GroupVersioned results = mDb.search("Amazon");
//assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
public void testSearch() {
GroupVersioned results = mDb.search("Amazon");
//assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
}
}
public void testBackupIncluded() {
updateOmitSetting(false);
GroupVersioned results = mDb.search("BackupOnly");
public void testBackupIncluded() {
updateOmitSetting(false);
GroupVersioned results = mDb.search("BackupOnly");
//assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
}
//assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
}
public void testBackupExcluded() {
updateOmitSetting(true);
GroupVersioned results = mDb.search("BackupOnly");
public void testBackupExcluded() {
updateOmitSetting(true);
GroupVersioned results = mDb.search("BackupOnly");
//assertFalse("Search result found, but should not have been.", results.numbersOfChildEntries() > 0);
}
//assertFalse("Search result found, but should not have been.", results.numbersOfChildEntries() > 0);
}
private void updateOmitSetting(boolean setting) {
Context ctx = getContext();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
SharedPreferences.Editor editor = prefs.edit();
private void updateOmitSetting(boolean setting) {
Context ctx = getContext();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("settings_omitbackup_key", setting);
editor.commit();
editor.putBoolean("settings_omitbackup_key", setting);
editor.commit();
}
}
}

View File

@@ -1,22 +1,22 @@
/*
* 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/>.
*
*/
* 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.tests.stream;
import static org.junit.Assert.assertArrayEquals;
@@ -35,76 +35,76 @@ import com.kunzisoft.keepass.stream.HashedBlockOutputStream;
public class HashedBlock extends TestCase {
private static Random rand = new Random();
private static Random rand = new Random();
public void testBlockAligned() throws IOException {
testSize(1024, 1024);
}
public void testBlockAligned() throws IOException {
testSize(1024, 1024);
}
public void testOffset() throws IOException {
testSize(1500, 1024);
}
public void testOffset() throws IOException {
testSize(1500, 1024);
}
private void testSize(int blockSize, int bufferSize) throws IOException {
byte[] orig = new byte[blockSize];
private void testSize(int blockSize, int bufferSize) throws IOException {
byte[] orig = new byte[blockSize];
rand.nextBytes(orig);
rand.nextBytes(orig);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HashedBlockOutputStream output = new HashedBlockOutputStream(bos, bufferSize);
output.write(orig);
output.close();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HashedBlockOutputStream output = new HashedBlockOutputStream(bos, bufferSize);
output.write(orig);
output.close();
byte[] encoded = bos.toByteArray();
byte[] encoded = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
HashedBlockInputStream input = new HashedBlockInputStream(bis);
ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
HashedBlockInputStream input = new HashedBlockInputStream(bis);
ByteArrayOutputStream decoded = new ByteArrayOutputStream();
while ( true ) {
byte[] buf = new byte[1024];
int read = input.read(buf);
if ( read == -1 ) {
break;
}
ByteArrayOutputStream decoded = new ByteArrayOutputStream();
while ( true ) {
byte[] buf = new byte[1024];
int read = input.read(buf);
if ( read == -1 ) {
break;
}
decoded.write(buf, 0, read);
}
decoded.write(buf, 0, read);
}
byte[] out = decoded.toByteArray();
byte[] out = decoded.toByteArray();
assertArrayEquals(orig, out);
assertArrayEquals(orig, out);
}
}
public void testGZIPStream() throws IOException {
final int testLength = 32000;
public void testGZIPStream() throws IOException {
final int testLength = 32000;
byte[] orig = new byte[testLength];
rand.nextBytes(orig);
byte[] orig = new byte[testLength];
rand.nextBytes(orig);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HashedBlockOutputStream hos = new HashedBlockOutputStream(bos);
GZIPOutputStream zos = new GZIPOutputStream(hos);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HashedBlockOutputStream hos = new HashedBlockOutputStream(bos);
GZIPOutputStream zos = new GZIPOutputStream(hos);
zos.write(orig);
zos.close();
zos.write(orig);
zos.close();
byte[] compressed = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(compressed);
HashedBlockInputStream his = new HashedBlockInputStream(bis);
GZIPInputStream zis = new GZIPInputStream(his);
byte[] compressed = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(compressed);
HashedBlockInputStream his = new HashedBlockInputStream(bis);
GZIPInputStream zis = new GZIPInputStream(his);
byte[] uncompressed = new byte[testLength];
byte[] uncompressed = new byte[testLength];
int read = 0;
while (read != -1 && testLength - read > 0) {
read += zis.read(uncompressed, read, testLength - read);
int read = 0;
while (read != -1 && testLength - read > 0) {
read += zis.read(uncompressed, read, testLength - read);
}
}
assertArrayEquals("Output not equal to input", orig, uncompressed);
assertArrayEquals("Output not equal to input", orig, uncompressed);
}
}
}

View File

@@ -30,28 +30,28 @@ public class StringUtilTest extends TestCase {
private final String search = "BcDe";
private final String badSearch = "Ed";
public void testIndexOfIgnoreCase1() {
assertEquals(1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH));
}
public void testIndexOfIgnoreCase1() {
assertEquals(1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH));
}
public void testIndexOfIgnoreCase2() {
assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH), 2);
}
public void testIndexOfIgnoreCase2() {
assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH), 2);
}
public void testIndexOfIgnoreCase3() {
assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH));
}
public void testIndexOfIgnoreCase3() {
assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH));
}
private final String repText = "AbCtestingaBc";
private final String repSearch = "ABc";
private final String repSearchBad = "CCCCCC";
private final String repNew = "12345";
private final String repResult = "12345testing12345";
public void testReplaceAllIgnoresCase1() {
assertEquals(repResult, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH));
}
private final String repText = "AbCtestingaBc";
private final String repSearch = "ABc";
private final String repSearchBad = "CCCCCC";
private final String repNew = "12345";
private final String repResult = "12345testing12345";
public void testReplaceAllIgnoresCase1() {
assertEquals(repResult, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH));
}
public void testReplaceAllIgnoresCase2() {
assertEquals(repText, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH));
}
public void testReplaceAllIgnoresCase2() {
assertEquals(repText, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH));
}
}

View File

@@ -27,7 +27,7 @@
android:value="" />
<activity
android:name="com.kunzisoft.keepass.fileselect.FileSelectActivity"
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
android:theme="@style/KeepassDXStyle.SplashScreen"
android:label="@string/app_name"
android:launchMode="singleTop"
@@ -39,7 +39,7 @@
</intent-filter>
</activity>
<activity
android:name="com.kunzisoft.keepass.password.PasswordActivity"
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
android:configChanges="orientation|keyboardHidden"
android:windowSoftInputMode="adjustResize">
<intent-filter>
@@ -89,7 +89,7 @@
android:resource="@xml/nnf_provider_paths" />
</provider>
<activity
android:name="com.kunzisoft.keepass.fileselect.FilePickerStylishActivity"
android:name=".activities.stylish.FilePickerStylishActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />

View File

@@ -1,88 +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;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.MenuItem;
import android.widget.TextView;
import com.kunzisoft.keepass.BuildConfig;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.stylish.StylishActivity;
import org.joda.time.DateTime;
public class AboutActivity extends StylishActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.about);
Toolbar toolbar = findViewById(R.id.toolbar);
toolbar.setTitle(getString(R.string.menu_about));
setSupportActionBar(toolbar);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
String version;
String build;
try {
version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
build = BuildConfig.BUILD_VERSION;
} catch (NameNotFoundException e) {
Log.w(getClass().getSimpleName(), "Unable to get the app or the build version", e);
version = "Unable to get the app version";
build = "Unable to get the build version";
}
version = getString(R.string.version_label, version);
TextView versionTextView = findViewById(R.id.activity_about_version);
versionTextView.setText(version);
build = getString(R.string.build_label, build);
TextView buildTextView = findViewById(R.id.activity_about_build);
buildTextView.setText(build);
TextView disclaimerText = findViewById(R.id.disclaimer);
disclaimerText.setText(getString(R.string.disclaimer_formal, new DateTime().getYear()));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
switch (id) {
case android.R.id.home:
finish();
break;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Bundle
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.MenuItem
import android.widget.TextView
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() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.about)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = getString(R.string.menu_about)
setSupportActionBar(toolbar)
assert(supportActionBar != null)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
supportActionBar!!.setDisplayShowHomeEnabled(true)
var version: String
var build: String
try {
version = packageManager.getPackageInfo(packageName, 0).versionName
build = BuildConfig.BUILD_VERSION
} catch (e: NameNotFoundException) {
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
version = "Unable to get the app version"
build = "Unable to get the build version"
}
version = getString(R.string.version_label, version)
val versionTextView = findViewById<TextView>(R.id.activity_about_version)
versionTextView.text = version
build = getString(R.string.build_label, build)
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
buildTextView.text = build
val disclaimerText = findViewById<TextView>(R.id.disclaimer)
disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
when (item.itemId) {
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
}
}

View File

@@ -1,523 +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;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.activities.lock.LockingHideActivity;
import com.kunzisoft.keepass.app.App;
import com.kunzisoft.keepass.database.element.Database;
import com.kunzisoft.keepass.database.element.EntryVersioned;
import com.kunzisoft.keepass.database.element.PwNodeId;
import com.kunzisoft.keepass.database.element.security.ProtectedString;
import com.kunzisoft.keepass.notifications.NotificationCopyingService;
import com.kunzisoft.keepass.notifications.NotificationField;
import com.kunzisoft.keepass.settings.PreferencesUtil;
import com.kunzisoft.keepass.settings.SettingsAutofillActivity;
import com.kunzisoft.keepass.timeout.ClipboardHelper;
import com.kunzisoft.keepass.timeout.TimeoutHelper;
import com.kunzisoft.keepass.utils.EmptyUtils;
import com.kunzisoft.keepass.utils.MenuUtil;
import com.kunzisoft.keepass.utils.Util;
import com.kunzisoft.keepass.view.EntryContentsView;
import java.util.ArrayList;
import java.util.Date;
import kotlin.Unit;
import kotlin.jvm.functions.Function2;
import static com.kunzisoft.keepass.settings.PreferencesUtil.isClipboardNotificationsEnable;
import static com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields;
public class EntryActivity extends LockingHideActivity {
private final static String TAG = EntryActivity.class.getName();
public static final String KEY_ENTRY = "entry";
private ImageView titleIconView;
private TextView titleView;
private EntryContentsView entryContentsView;
private Toolbar toolbar;
protected EntryVersioned mEntry;
private boolean mShowPassword;
private ClipboardHelper clipboardHelper;
private boolean firstLaunchOfActivity;
private int iconColor;
public static void launch(Activity activity, EntryVersioned pw, boolean readOnly) {
if (TimeoutHelper.INSTANCE.checkTimeAndLockIfTimeout(activity)) {
Intent intent = new Intent(activity, EntryActivity.class);
intent.putExtra(KEY_ENTRY, pw.getNodeId());
ReadOnlyHelper.INSTANCE.putReadOnlyInIntent(intent, readOnly);
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.entry_view);
toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
assert getSupportActionBar() != null;
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
Database db = App.getDB();
setReadOnly(db.isReadOnly() || getReadOnly());
mShowPassword = !PreferencesUtil.isPasswordMask(this);
// Get Entry from UUID
Intent i = getIntent();
PwNodeId keyEntry;
try {
keyEntry = i.getParcelableExtra(KEY_ENTRY);
mEntry = db.getEntryById(keyEntry);
} catch (ClassCastException e) {
Log.e(TAG, "Unable to retrieve the entry key");
}
if (mEntry == null) {
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show();
finish();
return;
}
// Update last access time.
mEntry.touch(false, false);
// Retrieve the textColor to tint the icon
int[] attrs = {R.attr.textColorInverse};
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
iconColor = ta.getColor(0, Color.WHITE);
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
invalidateOptionsMenu();
// Get views
titleIconView = findViewById(R.id.entry_icon);
titleView = findViewById(R.id.entry_title);
entryContentsView = findViewById(R.id.entry_contents);
entryContentsView.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this));
// Init the clipboard helper
clipboardHelper = new ClipboardHelper(this);
firstLaunchOfActivity = true;
}
@Override
protected void onResume() {
super.onResume();
// Fill data in resume to update from EntryEditActivity
fillData();
invalidateOptionsMenu();
Database database = App.getDB();
// Start to manage field reference to copy a value from ref
database.startManageEntry(mEntry);
boolean containsUsernameToCopy =
mEntry.getUsername().length() > 0;
boolean containsPasswordToCopy =
(mEntry.getPassword().length() > 0
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(this));
boolean containsExtraFieldToCopy =
(mEntry.allowExtraFields()
&& ((mEntry.containsCustomFields()
&& mEntry.containsCustomFieldsNotProtected())
|| (mEntry.containsCustomFields()
&& mEntry.containsCustomFieldsProtected()
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(this))
)
);
// If notifications enabled in settings
// Don't if application timeout
if (firstLaunchOfActivity && isClipboardNotificationsEnable(getApplicationContext())) {
if (containsUsernameToCopy
|| containsPasswordToCopy
|| containsExtraFieldToCopy
) {
// username already copied, waiting for user's action before copy password.
Intent intent = new Intent(this, NotificationCopyingService.class);
intent.setAction(NotificationCopyingService.ACTION_NEW_NOTIFICATION);
if (mEntry.getTitle() != null)
intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE, mEntry.getTitle());
// Construct notification fields
ArrayList<NotificationField> notificationFields = new ArrayList<>();
// Add username if exists to notifications
if (containsUsernameToCopy)
notificationFields.add(
new NotificationField(
NotificationField.NotificationFieldId.USERNAME,
mEntry.getUsername(),
getResources()));
// Add password to notifications
if (containsPasswordToCopy) {
notificationFields.add(
new NotificationField(
NotificationField.NotificationFieldId.PASSWORD,
mEntry.getPassword(),
getResources()));
}
// Add extra fields
if (containsExtraFieldToCopy) {
try {
mEntry.getFields().doActionToAllCustomProtectedField(new Function2<String, ProtectedString, Unit>() {
private int anonymousFieldNumber = 0;
@Override
public Unit invoke(String key, ProtectedString value) {
//If value is not protected or allowed
if (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this)) {
notificationFields.add(
new NotificationField(
NotificationField.NotificationFieldId.getAnonymousFieldId()[anonymousFieldNumber],
value.toString(),
key,
getResources()));
anonymousFieldNumber++;
}
return null;
}
});
} catch (ArrayIndexOutOfBoundsException e) {
Log.w(TAG, "Only " + NotificationField.NotificationFieldId.getAnonymousFieldId().length +
" anonymous notifications are available");
}
}
// Add notifications
intent.putParcelableArrayListExtra(NotificationCopyingService.EXTRA_FIELDS, notificationFields);
startService(intent);
}
database.stopManageEntry(mEntry);
}
firstLaunchOfActivity = false;
}
/**
* Check and display learning views
* Displays the explanation for copying a field and editing an entry
*/
private void checkAndPerformedEducation(Menu menu) {
if (PreferencesUtil.isEducationScreensEnabled(this)) {
if (entryContentsView != null && entryContentsView.isUserNamePresent()
&& !PreferencesUtil.isEducationCopyUsernamePerformed(this)) {
TapTargetView.showFor(this,
TapTarget.forView(findViewById(R.id.entry_user_name_action_image),
getString(R.string.education_field_copy_title),
getString(R.string.education_field_copy_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(),
getString(R.string.copy_field, getString(R.string.entry_user_name)));
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
// Launch autofill settings
startActivity(new Intent(EntryActivity.this, SettingsAutofillActivity.class));
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_copy_username_key);
} else if (!PreferencesUtil.isEducationEntryEditPerformed(this)) {
try {
TapTargetView.showFor(this,
TapTarget.forToolbarMenuItem(toolbar, R.id.menu_edit,
getString(R.string.education_entry_edit_title),
getString(R.string.education_entry_edit_summary))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
MenuItem editItem = menu.findItem(R.id.menu_edit);
onOptionsItemSelected(editItem);
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
// Open Keepass doc to create field references
Intent browserIntent = new Intent(Intent.ACTION_VIEW,
Uri.parse(getString(R.string.field_references_url)));
startActivity(browserIntent);
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_entry_edit_key);
} catch (Exception e) {
// If icon not visible
Log.w(TAG, "Can't performed education for entry's edition");
}
}
}
}
protected void fillData() {
Database database = App.getDB();
database.startManageEntry(mEntry);
// Assign title icon
database.getDrawFactory().assignDatabaseIconTo(this, titleIconView, mEntry.getIcon(), iconColor);
// Assign title text
titleView.setText(mEntry.getVisualTitle());
// Assign basic fields
entryContentsView.assignUserName(mEntry.getUsername());
entryContentsView.assignUserNameCopyListener(view ->
clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(),
getString(R.string.copy_field, getString(R.string.entry_user_name)))
);
boolean allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this);
entryContentsView.assignPassword(mEntry.getPassword(), allowCopyPassword);
if (allowCopyPassword) {
entryContentsView.assignPasswordCopyListener(view ->
clipboardHelper.timeoutCopyToClipboard(mEntry.getPassword(),
getString(R.string.copy_field, getString(R.string.entry_password)))
);
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) {
entryContentsView.assignPasswordCopyListener(v -> {
String message = getString(R.string.allow_copy_password_warning) +
"\n\n" +
getString(R.string.clipboard_warning);
AlertDialog warningDialog = new AlertDialog.Builder(EntryActivity.this)
.setMessage(message).create();
warningDialog.setButton(AlertDialog.BUTTON1, getText(android.R.string.ok),
(dialog, which) -> {
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(EntryActivity.this, true);
dialog.dismiss();
fillData();
});
warningDialog.setButton(AlertDialog.BUTTON2, getText(android.R.string.cancel),
(dialog, which) -> {
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(EntryActivity.this, false);
dialog.dismiss();
fillData();
});
warningDialog.show();
});
} else {
entryContentsView.assignPasswordCopyListener(null);
}
}
entryContentsView.assignURL(mEntry.getUrl());
entryContentsView.setHiddenPasswordStyle(!mShowPassword);
entryContentsView.assignComment(mEntry.getNotes());
// Assign custom fields
if (mEntry.allowExtraFields()) {
entryContentsView.clearExtraFields();
mEntry.getFields().doActionToAllCustomProtectedField((label, value) -> {
boolean showAction = (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this));
entryContentsView.addExtraField(label, value, showAction, view ->
clipboardHelper.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
);
return null;
});
}
// Assign dates
entryContentsView.assignCreationDate(mEntry.getCreationTime().getDate());
entryContentsView.assignModificationDate(mEntry.getLastModificationTime().getDate());
entryContentsView.assignLastAccessDate(mEntry.getLastAccessTime().getDate());
Date expires = mEntry.getExpiryTime().getDate();
if ( mEntry.isExpires() ) {
entryContentsView.assignExpiresDate(expires);
} else {
entryContentsView.assignExpiresDate(getString(R.string.never));
}
database.stopManageEntry(mEntry);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE:
// Not directly get the entry from intent data but from database
fillData();
break;
}
}
private void changeShowPasswordIcon(MenuItem togglePassword) {
if ( mShowPassword ) {
togglePassword.setTitle(R.string.menu_hide_password);
togglePassword.setIcon(R.drawable.ic_visibility_off_white_24dp);
} else {
togglePassword.setTitle(R.string.menu_showpass);
togglePassword.setIcon(R.drawable.ic_visibility_white_24dp);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
MenuUtil.INSTANCE.contributionMenuInflater(inflater, menu);
inflater.inflate(R.menu.entry, menu);
inflater.inflate(R.menu.database_lock, menu);
if (getReadOnly()) {
MenuItem edit = menu.findItem(R.id.menu_edit);
if (edit != null)
edit.setVisible(false);
}
MenuItem togglePassword = menu.findItem(R.id.menu_toggle_pass);
if (entryContentsView != null && togglePassword != null) {
if (entryContentsView.isPasswordPresent() || entryContentsView.atLeastOneFieldProtectedPresent()) {
changeShowPasswordIcon(togglePassword);
} else {
togglePassword.setVisible(false);
}
}
MenuItem gotoUrl = menu.findItem(R.id.menu_goto_url);
if (gotoUrl != null) {
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
// so mEntry may not be set
if (mEntry == null) {
gotoUrl.setVisible(false);
} else {
String url = mEntry.getUrl();
if (EmptyUtils.isNullOrEmpty(url)) {
// disable button if url is not available
gotoUrl.setVisible(false);
}
}
}
// Show education views
new Handler().post(() -> checkAndPerformedEducation(menu));
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ) {
case R.id.menu_contribute:
return MenuUtil.INSTANCE.onContributionItemSelected(this);
case R.id.menu_toggle_pass:
mShowPassword = !mShowPassword;
changeShowPasswordIcon(item);
entryContentsView.setHiddenPasswordStyle(!mShowPassword);
return true;
case R.id.menu_edit:
EntryEditActivity.launch(EntryActivity.this, mEntry);
return true;
case R.id.menu_goto_url:
String url;
url = mEntry.getUrl();
// Default http:// if no protocol specified
if ( ! url.contains("://") ) {
url = "http://" + url;
}
try {
Util.gotoUrl(this, url);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show();
}
return true;
case R.id.menu_lock:
lockAndExit();
return true;
case android.R.id.home :
finish(); // close this activity and return to preview activity (if there is any)
}
return super.onOptionsItemSelected(item);
}
@Override
public void finish() {
// Transit data in previous Activity after an update
/*
TODO Slowdown when add entry as result
Intent intent = new Intent();
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
*/
super.finish();
}
}

View File

@@ -0,0 +1,392 @@
/*
* 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
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.support.v7.app.AlertDialog
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.view.EntryContentsView
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.PwNodeId
import com.kunzisoft.keepass.notifications.NotificationEntryCopyManager
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.Util
class EntryActivity : LockingHideActivity() {
private var titleIconView: ImageView? = null
private var titleView: TextView? = null
private var entryContentsView: EntryContentsView? = null
private var toolbar: Toolbar? = null
private var mEntry: EntryVersioned? = null
private var mShowPassword: Boolean = false
private var clipboardHelper: ClipboardHelper? = null
private var firstLaunchOfActivity: Boolean = false
private var iconColor: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.entry_view)
toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
val currentDatabase = App.currentDatabase
readOnly = currentDatabase.isReadOnly || readOnly
mShowPassword = !PreferencesUtil.isPasswordMask(this)
// Get Entry from UUID
try {
val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY)
mEntry = currentDatabase.getEntryById(keyEntry)
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
if (mEntry == null) {
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
finish()
return
}
// Update last access time.
mEntry?.touch(modified = false, touchParents = false)
// Retrieve the textColor to tint the icon
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
iconColor = taIconColor.getColor(0, Color.WHITE)
taIconColor.recycle()
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
invalidateOptionsMenu()
// Get views
titleIconView = findViewById(R.id.entry_icon)
titleView = findViewById(R.id.entry_title)
entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
// Init the clipboard helper
clipboardHelper = ClipboardHelper(this)
firstLaunchOfActivity = true
}
override fun onResume() {
super.onResume()
mEntry?.let { entry ->
// Fill data in resume to update from EntryEditActivity
fillEntryDataInContentsView(entry)
// Refresh Menu
invalidateOptionsMenu()
// Manage entry copy to start notification if allowed
NotificationEntryCopyManager.launchNotificationIfAllowed(this,
firstLaunchOfActivity,
entry)
}
firstLaunchOfActivity = false
}
private fun fillEntryDataInContentsView(entry: EntryVersioned) {
val database = App.currentDatabase
database.startManageEntry(entry)
// Assign title icon
database.drawFactory.assignDatabaseIconTo(this, titleIconView, entry.icon, iconColor)
// Assign title text
titleView?.text = entry.getVisualTitle()
// Assign basic fields
entryContentsView?.assignUserName(entry.username)
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(entry.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
})
val allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
entryContentsView?.assignPassword(entry.password, allowCopyPassword)
if (allowCopyPassword) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(entry.password,
getString(R.string.copy_field,
getString(R.string.entry_password)))
})
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
val message = getString(R.string.allow_copy_password_warning) +
"\n\n" +
getString(R.string.clipboard_warning)
val warningDialog = AlertDialog.Builder(this@EntryActivity)
.setMessage(message).create()
warningDialog.setButton(AlertDialog.BUTTON_POSITIVE, getText(android.R.string.ok)
) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
warningDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getText(android.R.string.cancel)
) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
warningDialog.show()
})
} else {
entryContentsView?.assignPasswordCopyListener(null)
}
}
entryContentsView?.assignURL(entry.url)
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
entryContentsView?.assignComment(entry.notes)
// Assign custom fields
if (entry.allowExtraFields()) {
entryContentsView?.clearExtraFields()
entry.fields.doActionToAllCustomProtectedField { label, value ->
val showAction = !value.isProtected || PreferencesUtil.allowCopyPasswordAndProtectedFields(this@EntryActivity)
entryContentsView?.addExtraField(label, value, showAction, View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
})
}
}
// Assign dates
entry.creationTime.date?.let {
entryContentsView?.assignCreationDate(it)
}
entry.lastModificationTime.date?.let {
entryContentsView?.assignModificationDate(it)
}
entry.lastAccessTime.date?.let {
entryContentsView?.assignLastAccessDate(it)
}
val expires = entry.expiryTime.date
if (entry.isExpires && expires != null) {
entryContentsView?.assignExpiresDate(expires)
} else {
entryContentsView?.assignExpiresDate(getString(R.string.never))
}
database.stopManageEntry(entry)
}
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
mEntry?.let {
fillEntryDataInContentsView(it)
}
}
}
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
if (mShowPassword) {
togglePassword?.setTitle(R.string.menu_hide_password)
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
} else {
togglePassword?.setTitle(R.string.menu_showpass)
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database_lock, menu)
if (readOnly) {
menu.findItem(R.id.menu_edit)?.isVisible = false
}
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
entryContentsView?.let {
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
changeShowPasswordIcon(togglePassword)
} else {
togglePassword?.isVisible = false
}
}
val gotoUrl = menu.findItem(R.id.menu_goto_url)
gotoUrl?.apply {
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
// so mEntry may not be set
if (mEntry == null) {
isVisible = false
} else {
if (mEntry?.url?.isEmpty() != false) {
// disable button if url is not available
isVisible = false
}
}
}
// Show education views
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
return true
}
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
menu: Menu) {
if (entryContentsView?.isUserNamePresent == true
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
findViewById(R.id.entry_user_name_action_image),
{
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
},
{
// Launch autofill settings
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
}))
else if (toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
toolbar!!.findViewById(R.id.menu_edit),
{
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
},
{
// Open Keepass doc to create field references
startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse(getString(R.string.field_references_url))))
}))
;
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
R.id.menu_toggle_pass -> {
mShowPassword = !mShowPassword
changeShowPasswordIcon(item)
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
return true
}
R.id.menu_edit -> {
mEntry?.let {
EntryEditActivity.launch(this@EntryActivity, it)
}
return true
}
R.id.menu_goto_url -> {
var url: String = mEntry?.url ?: ""
// Default http:// if no protocol specified
if (!url.contains("://")) {
url = "http://$url"
}
try {
Util.gotoUrl(this, url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show()
}
return true
}
R.id.menu_lock -> {
lockAndExit()
return true
}
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
}
return super.onOptionsItemSelected(item)
}
override fun finish() {
// Transit data in previous Activity after an update
/*
TODO Slowdown when add entry as result
Intent intent = new Intent();
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
*/
super.finish()
}
companion object {
private val TAG = EntryActivity::class.java.name
const val KEY_ENTRY = "entry"
fun launch(activity: Activity, pw: EntryVersioned, readOnly: Boolean) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, pw.nodeId)
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
}
}
}
}

View File

@@ -1,581 +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;
import android.app.Activity;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.*;
import android.widget.*;
import com.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.activities.lock.LockingHideActivity;
import com.kunzisoft.keepass.app.App;
import com.kunzisoft.keepass.database.action.node.ActionNodeValues;
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable;
import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable;
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable;
import com.kunzisoft.keepass.database.element.*;
import com.kunzisoft.keepass.database.element.security.ProtectedString;
import com.kunzisoft.keepass.dialogs.GeneratePasswordDialogFragment;
import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment;
import com.kunzisoft.keepass.settings.PreferencesUtil;
import com.kunzisoft.keepass.tasks.ActionRunnable;
import com.kunzisoft.keepass.timeout.TimeoutHelper;
import com.kunzisoft.keepass.utils.MenuUtil;
import com.kunzisoft.keepass.utils.Util;
import com.kunzisoft.keepass.view.EntryEditCustomField;
import org.jetbrains.annotations.NotNull;
import static com.kunzisoft.keepass.dialogs.IconPickerDialogFragment.KEY_ICON_STANDARD;
public class EntryEditActivity extends LockingHideActivity
implements IconPickerDialogFragment.IconPickerListener,
GeneratePasswordDialogFragment.GeneratePasswordListener {
private static final String TAG = EntryEditActivity.class.getName();
// Keys for current Activity
public static final String KEY_ENTRY = "entry";
public static final String KEY_PARENT = "parent";
// Keys for callback
public static final int ADD_ENTRY_RESULT_CODE = 31;
public static final int UPDATE_ENTRY_RESULT_CODE = 32;
public static final int ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129;
public static final String ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY";
private Database database;
protected EntryVersioned mEntry;
protected GroupVersioned mParent;
protected EntryVersioned mNewEntry;
protected boolean mIsNew;
protected PwIconStandard mSelectedIconStandard;
// Views
private ScrollView scrollView;
private EditText entryTitleView;
private ImageView entryIconView;
private EditText entryUserNameView;
private EditText entryUrlView;
private EditText entryPasswordView;
private EditText entryConfirmationPasswordView;
private View generatePasswordView;
private EditText entryCommentView;
private ViewGroup entryExtraFieldsContainer;
private View addNewFieldView;
private View saveView;
private int iconColor;
/**
* Launch EntryEditActivity to update an existing entry
*
* @param activity from activity
* @param pwEntry Entry to update
*/
public static void launch(Activity activity, EntryVersioned pwEntry) {
if (TimeoutHelper.INSTANCE.checkTimeAndLockIfTimeout(activity)) {
Intent intent = new Intent(activity, EntryEditActivity.class);
intent.putExtra(KEY_ENTRY, pwEntry.getNodeId());
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE);
}
}
/**
* Launch EntryEditActivity to add a new entry
*
* @param activity from activity
* @param pwGroup Group who will contains new entry
*/
public static void launch(Activity activity, GroupVersioned pwGroup) {
if (TimeoutHelper.INSTANCE.checkTimeAndLockIfTimeout(activity)) {
Intent intent = new Intent(activity, EntryEditActivity.class);
intent.putExtra(KEY_PARENT, pwGroup.getNodeId());
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.entry_edit);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
scrollView = findViewById(R.id.entry_edit_scroll);
scrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
entryTitleView = findViewById(R.id.entry_edit_title);
entryIconView = findViewById(R.id.entry_edit_icon_button);
entryUserNameView = findViewById(R.id.entry_edit_user_name);
entryUrlView = findViewById(R.id.entry_edit_url);
entryPasswordView = findViewById(R.id.entry_edit_password);
entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password);
entryCommentView = findViewById(R.id.entry_edit_notes);
entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container);
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(
entryTitleView,
entryIconView,
entryUserNameView,
entryUrlView,
entryPasswordView,
entryConfirmationPasswordView,
entryCommentView,
entryExtraFieldsContainer);
// Likely the app has been killed exit the activity
database = App.getDB();
// Retrieve the textColor to tint the icon
int[] attrs = {android.R.attr.textColorPrimary};
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
iconColor = ta.getColor(0, Color.WHITE);
mSelectedIconStandard = database.getIconFactory().getUnknownIcon();
Intent intent = getIntent();
// Entry is retrieve, it's an entry to update
PwNodeId keyEntry = intent.getParcelableExtra(KEY_ENTRY);
if (keyEntry != null) {
mIsNew = false;
mEntry = database.getEntryById(keyEntry);
if (mEntry != null) {
mParent = mEntry.getParent();
fillData();
}
}
// Parent is retrieve, it's a new entry to create
PwNodeId keyParent = intent.getParcelableExtra(KEY_PARENT);
if (keyParent != null) {
mIsNew = true;
mEntry = database.createEntry();
mParent = database.getGroupById(keyParent);
// Add the default icon
database.getDrawFactory().assignDefaultDatabaseIconTo(this, entryIconView, iconColor);
}
// Close the activity if entry or parent can't be retrieve
if (mEntry == null || mParent == null) {
finish();
return;
}
// Assign title
setTitle((mIsNew) ? getString(R.string.add_entry) : getString(R.string.edit_entry));
// Retrieve the icon after an orientation change
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_ICON_STANDARD)) {
iconPicked(savedInstanceState);
}
// Add listener to the icon
entryIconView.setOnClickListener(v ->
IconPickerDialogFragment.launch(EntryEditActivity.this));
// Generate password button
generatePasswordView = findViewById(R.id.entry_edit_generate_button);
generatePasswordView.setOnClickListener(v -> openPasswordGenerator());
// Save button
saveView = findViewById(R.id.entry_edit_save);
saveView.setOnClickListener(v -> saveEntry());
if (mEntry.allowExtraFields()) {
addNewFieldView = findViewById(R.id.entry_edit_add_new_field);
addNewFieldView.setVisibility(View.VISIBLE);
addNewFieldView.setOnClickListener(v -> addNewCustomField());
}
// Verify the education views
checkAndPerformedEducation();
}
/**
* Open the password generator fragment
*/
private void openPasswordGenerator() {
GeneratePasswordDialogFragment generatePasswordDialogFragment = new GeneratePasswordDialogFragment();
generatePasswordDialogFragment.show(getSupportFragmentManager(), "PasswordGeneratorFragment");
}
/**
* Add a new view to fill in the information of the customized field
*/
private void addNewCustomField() {
EntryEditCustomField entryEditCustomField = new EntryEditCustomField(EntryEditActivity.this);
entryEditCustomField.setData("", new ProtectedString(false, ""));
boolean visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this);
entryEditCustomField.setFontVisibility(visibilityFontActivated);
entryExtraFieldsContainer.addView(entryEditCustomField);
// Scroll bottom
scrollView.post(() -> scrollView.fullScroll(ScrollView.FOCUS_DOWN));
}
/**
* Saves the new entry or update an existing entry in the database
*/
private void saveEntry() {
if (!validateBeforeSaving()) {
return;
}
// Clone the entry
mNewEntry = new EntryVersioned(mEntry);
populateEntryWithViewInfo(mNewEntry);
// Open a progress dialog and save entry
ActionRunnable task;
AfterActionNodeFinishRunnable afterActionNodeFinishRunnable =
new AfterActionNodeFinishRunnable() {
@Override
public void onActionNodeFinish(@NotNull ActionNodeValues actionNodeValues) {
if (actionNodeValues.getSuccess())
finish();
}
};
if ( mIsNew ) {
task = new AddEntryRunnable(EntryEditActivity.this,
database,
mNewEntry,
mParent,
afterActionNodeFinishRunnable,
!getReadOnly());
} else {
task = new UpdateEntryRunnable(EntryEditActivity.this,
database,
mEntry,
mNewEntry,
afterActionNodeFinishRunnable,
!getReadOnly());
}
new Thread(task).start();
}
/**
* Check and display learning views
* Displays the explanation for the icon selection, the password generator and for a new field
*/
private void checkAndPerformedEducation() {
if (PreferencesUtil.isEducationScreensEnabled(this)) {
// TODO Show icon
if (!PreferencesUtil.isEducationPasswordGeneratorPerformed(this)) {
TapTargetView.showFor(this,
TapTarget.forView(generatePasswordView,
getString(R.string.education_generate_password_title),
getString(R.string.education_generate_password_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
openPasswordGenerator();
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_password_generator_key);
} else if (mEntry.allowExtraFields()
&& !mEntry.containsCustomFields()
&& !PreferencesUtil.isEducationEntryNewFieldPerformed(this)) {
TapTargetView.showFor(this,
TapTarget.forView(addNewFieldView,
getString(R.string.education_entry_new_field_title),
getString(R.string.education_entry_new_field_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
addNewCustomField();
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_entry_new_field_key);
}
}
}
/**
* Utility class to retrieve a validation or an error with a message
*/
private class ErrorValidation {
static final int unknownMessage = -1;
boolean isValidate = false;
int messageId = unknownMessage;
void showValidationErrorIfNeeded() {
if (!isValidate && messageId != unknownMessage)
Toast.makeText(EntryEditActivity.this, messageId, Toast.LENGTH_LONG).show();
}
}
/**
* Validate or not the entry form
*
* @return ErrorValidation An error with a message or a validation without message
*/
protected ErrorValidation validate() {
ErrorValidation errorValidation = new ErrorValidation();
// Require title
String title = entryTitleView.getText().toString();
if ( title.length() == 0 ) {
errorValidation.messageId = R.string.error_title_required;
return errorValidation;
}
// Validate password
String pass = entryPasswordView.getText().toString();
String conf = entryConfirmationPasswordView.getText().toString();
if ( ! pass.equals(conf) ) {
errorValidation.messageId = R.string.error_pass_match;
return errorValidation;
}
// Validate extra fields
if (mEntry.allowExtraFields()) {
for (int i = 0; i < entryExtraFieldsContainer.getChildCount(); i++) {
EntryEditCustomField entryEditCustomField = (EntryEditCustomField) entryExtraFieldsContainer.getChildAt(i);
String key = entryEditCustomField.getLabel();
if (key == null || key.length() == 0) {
errorValidation.messageId = R.string.error_string_key;
return errorValidation;
}
}
}
errorValidation.isValidate = true;
return errorValidation;
}
/**
* Launch a validation with {@link #validate()} and show the error if present
*
* @return true if the form was validate or false if not
*/
protected boolean validateBeforeSaving() {
ErrorValidation errorValidation = validate();
errorValidation.showValidationErrorIfNeeded();
return errorValidation.isValidate;
}
private void populateEntryWithViewInfo(EntryVersioned newEntry) {
database.startManageEntry(newEntry);
newEntry.setLastAccessTime(new PwDate());
newEntry.setLastModificationTime(new PwDate());
newEntry.setTitle(entryTitleView.getText().toString());
newEntry.setIcon(retrieveIcon());
newEntry.setUrl(entryUrlView.getText().toString());
newEntry.setUsername(entryUserNameView.getText().toString());
newEntry.setNotes(entryCommentView.getText().toString());
newEntry.setPassword(entryPasswordView.getText().toString());
if (newEntry.allowExtraFields()) {
// Delete all extra strings
newEntry.removeAllCustomFields();
// Add extra fields from views
for (int i = 0; i < entryExtraFieldsContainer.getChildCount(); i++) {
EntryEditCustomField view = (EntryEditCustomField) entryExtraFieldsContainer.getChildAt(i);
String key = view.getLabel();
String value = view.getValue();
boolean protect = view.isProtected();
newEntry.addExtraField(key, new ProtectedString(protect, value));
}
}
database.stopManageEntry(newEntry);
}
/**
* Retrieve the icon by the selection, or the first icon in the list if the entry is new or the last one
*/
private PwIcon retrieveIcon() {
if (!mSelectedIconStandard.isUnknown())
return mSelectedIconStandard;
else {
if (mIsNew) {
return database.getIconFactory().getKeyIcon();
}
else {
// Keep previous icon, if no new one was selected
return mEntry.getIcon();
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.database_lock, menu);
MenuUtil.INSTANCE.contributionMenuInflater(inflater, menu);
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ) {
case R.id.menu_lock:
lockAndExit();
return true;
case R.id.menu_contribute:
return MenuUtil.INSTANCE.onContributionItemSelected(this);
case android.R.id.home:
finish();
}
return super.onOptionsItemSelected(item);
}
private void assignIconView() {
database.getDrawFactory()
.assignDatabaseIconTo(
this,
entryIconView,
mEntry.getIcon(),
iconColor);
}
protected void fillData() {
assignIconView();
// Don't start the field reference manager, we want to see the raw ref
App.getDB().stopManageEntry(mEntry);
entryTitleView.setText(mEntry.getTitle());
entryUserNameView.setText(mEntry.getUsername());
entryUrlView.setText(mEntry.getUrl());
String password = mEntry.getPassword();
entryPasswordView.setText(password);
entryConfirmationPasswordView.setText(password);
entryCommentView.setText(mEntry.getNotes());
boolean visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this);
if (visibilityFontActivated) {
Util.applyFontVisibilityTo(this, entryUserNameView);
Util.applyFontVisibilityTo(this, entryPasswordView);
Util.applyFontVisibilityTo(this, entryConfirmationPasswordView);
Util.applyFontVisibilityTo(this, entryCommentView);
}
if (mEntry.allowExtraFields()) {
LinearLayout container = findViewById(R.id.entry_edit_advanced_container);
mEntry.getFields().doActionToAllCustomProtectedField((key, value) -> {
EntryEditCustomField entryEditCustomField = new EntryEditCustomField(EntryEditActivity.this);
entryEditCustomField.setData(key, value);
entryEditCustomField.setFontVisibility(visibilityFontActivated);
container.addView(entryEditCustomField);
return null;
});
}
}
@Override
public void iconPicked(Bundle bundle) {
mSelectedIconStandard = bundle.getParcelable(KEY_ICON_STANDARD);
mEntry.setIcon(mSelectedIconStandard);
assignIconView();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if (!mSelectedIconStandard.isUnknown()) {
outState.putParcelable(KEY_ICON_STANDARD, mSelectedIconStandard);
super.onSaveInstanceState(outState);
}
}
@Override
public void acceptPassword(Bundle bundle) {
String generatedPassword = bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID);
entryPasswordView.setText(generatedPassword);
entryConfirmationPasswordView.setText(generatedPassword);
checkAndPerformedEducation();
}
@Override
public void cancelPassword(Bundle bundle) {
// Do nothing here
}
@Override
public void finish() {
// Assign entry callback as a result in all case
try {
if (mNewEntry != null) {
Bundle bundle = new Bundle();
Intent intentEntry = new Intent();
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry);
intentEntry.putExtras(bundle);
if (mIsNew) {
setResult(ADD_ENTRY_RESULT_CODE, intentEntry);
} else {
setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry);
}
}
super.finish();
} catch (Exception e) {
// Exception when parcelable can't be done
Log.e(TAG, "Cant add entry as result", e);
}
}
}

View File

@@ -0,0 +1,533 @@
/*
* 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
import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.*
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment.Companion.KEY_ICON_STANDARD
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
import com.kunzisoft.keepass.view.EntryEditCustomField
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
import com.kunzisoft.keepass.database.action.node.ActionNodeValues
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable
import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.Util
class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener {
private var mDatabase: Database? = null
private var mEntry: EntryVersioned? = null
private var mParent: GroupVersioned? = null
private var mNewEntry: EntryVersioned? = null
private var mIsNew: Boolean = false
private var mSelectedIconStandard: PwIconStandard? = null
// Views
private var scrollView: ScrollView? = null
private var entryTitleView: EditText? = null
private var entryIconView: ImageView? = null
private var entryUserNameView: EditText? = null
private var entryUrlView: EditText? = null
private var entryPasswordView: EditText? = null
private var entryConfirmationPasswordView: EditText? = null
private var generatePasswordView: View? = null
private var entryCommentView: EditText? = null
private var entryExtraFieldsContainer: ViewGroup? = null
private var addNewFieldView: View? = null
private var saveView: View? = null
private var iconColor: Int = 0
// View validation message
private var validationErrorMessageId = UNKNOWN_MESSAGE
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.entry_edit)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
scrollView = findViewById(R.id.entry_edit_scroll)
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
entryTitleView = findViewById(R.id.entry_edit_title)
entryIconView = findViewById(R.id.entry_edit_icon_button)
entryUserNameView = findViewById(R.id.entry_edit_user_name)
entryUrlView = findViewById(R.id.entry_edit_url)
entryPasswordView = findViewById(R.id.entry_edit_password)
entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password)
entryCommentView = findViewById(R.id.entry_edit_notes)
entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container)
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(
entryTitleView,
entryIconView,
entryUserNameView,
entryUrlView,
entryPasswordView,
entryConfirmationPasswordView,
entryCommentView,
entryExtraFieldsContainer)
// Likely the app has been killed exit the activity
mDatabase = App.currentDatabase
// Retrieve the textColor to tint the icon
val taIconColor = theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
iconColor = taIconColor.getColor(0, Color.WHITE)
taIconColor.recycle()
mSelectedIconStandard = mDatabase?.iconFactory?.unknownIcon
// Entry is retrieve, it's an entry to update
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let {
mIsNew = false
mEntry = mDatabase?.getEntryById(it)
mEntry?.let { entry ->
mParent = entry.parent
fillEntryDataInContentsView(entry)
}
}
// Parent is retrieve, it's a new entry to create
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
mIsNew = true
mEntry = mDatabase?.createEntry()
mParent = mDatabase?.getGroupById(it)
// Add the default icon
mDatabase?.drawFactory?.assignDefaultDatabaseIconTo(this, entryIconView, iconColor)
}
// Close the activity if entry or parent can't be retrieve
if (mEntry == null || mParent == null) {
finish()
return
}
// Assign title
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
// Retrieve the icon after an orientation change
savedInstanceState?.let {
if (it.containsKey(KEY_ICON_STANDARD)) {
iconPicked(it)
}
}
// Add listener to the icon
entryIconView?.setOnClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
// Generate password button
generatePasswordView = findViewById(R.id.entry_edit_generate_button)
generatePasswordView?.setOnClickListener { openPasswordGenerator() }
// Save button
saveView = findViewById(R.id.entry_edit_save)
mEntry?.let { entry ->
saveView?.setOnClickListener { saveEntry(entry) }
}
if (mEntry?.allowExtraFields() == true) {
addNewFieldView = findViewById(R.id.entry_edit_add_new_field)
addNewFieldView?.apply {
visibility = View.VISIBLE
setOnClickListener { addNewCustomField() }
}
}
// Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
}
}
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
if (generatePasswordView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
generatePasswordView!!,
{
openPasswordGenerator()
},
{
performedNextEducation(entryEditActivityEducation)
}
))
else if (mEntry != null
&& mEntry!!.allowExtraFields()
&& !mEntry!!.containsCustomFields()
&& addNewFieldView != null
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView!!,
{
addNewCustomField()
}))
;
}
/**
* Open the password generator fragment
*/
private fun openPasswordGenerator() {
GeneratePasswordDialogFragment().show(supportFragmentManager, "PasswordGeneratorFragment")
}
/**
* Add a new view to fill in the information of the customized field
*/
private fun addNewCustomField() {
val entryEditCustomField = EntryEditCustomField(this@EntryEditActivity)
entryEditCustomField.setData("", ProtectedString(false, ""))
val visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this)
entryEditCustomField.setFontVisibility(visibilityFontActivated)
entryExtraFieldsContainer?.addView(entryEditCustomField)
// Scroll bottom
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
}
/**
* Saves the new entry or update an existing entry in the database
*/
private fun saveEntry(entry: EntryVersioned) {
// Launch a validation and show the error if present
if (!isValid()) {
if (validationErrorMessageId != UNKNOWN_MESSAGE)
Toast.makeText(this@EntryEditActivity, validationErrorMessageId, Toast.LENGTH_LONG).show()
return
}
// Clone the entry
mDatabase?.let { database ->
mNewEntry = EntryVersioned(entry)
mNewEntry?.let { newEntry ->
populateEntryWithViewInfo(newEntry)
// Open a progress dialog and save entry
var actionRunnable: ActionRunnable? = null
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
if (actionNodeValues.success)
finish()
}
}
if (mIsNew) {
mParent?.let { parent ->
actionRunnable = AddEntryRunnable(this@EntryEditActivity,
database,
newEntry,
parent,
afterActionNodeFinishRunnable,
!readOnly)
}
} else {
actionRunnable = UpdateEntryRunnable(this@EntryEditActivity,
database,
entry,
newEntry,
afterActionNodeFinishRunnable,
!readOnly)
}
actionRunnable?.let { runnable ->
ProgressDialogSaveDatabaseThread(this@EntryEditActivity) {runnable}.start()
}
}
}
}
/**
* Validate or not the entry form
*
* @return ErrorValidation An error with a message or a validation without message
*/
private fun isValid(): Boolean {
// Require title
if (entryTitleView?.text.toString().isEmpty()) {
validationErrorMessageId = R.string.error_title_required
return false
}
// Validate password
if (entryPasswordView?.text.toString() != entryConfirmationPasswordView?.text.toString()) {
validationErrorMessageId = R.string.error_pass_match
return false
}
// Validate extra fields
if (mEntry?.allowExtraFields() == true) {
entryExtraFieldsContainer?.let {
for (i in 0 until it.childCount) {
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
val key = entryEditCustomField.label
if (key == null || key.isEmpty()) {
validationErrorMessageId = R.string.error_string_key
return false
}
}
}
}
return true
}
private fun populateEntryWithViewInfo(newEntry: EntryVersioned) {
mDatabase?.startManageEntry(newEntry)
newEntry.lastAccessTime = PwDate()
newEntry.lastModificationTime = PwDate()
newEntry.title = entryTitleView?.text.toString()
newEntry.icon = retrieveIcon()
newEntry.url = entryUrlView?.text.toString()
newEntry.username = entryUserNameView?.text.toString()
newEntry.notes = entryCommentView?.text.toString()
newEntry.password = entryPasswordView?.text.toString()
if (newEntry.allowExtraFields()) {
// Delete all extra strings
newEntry.removeAllCustomFields()
// Add extra fields from views
entryExtraFieldsContainer?.let {
for (i in 0 until it.childCount) {
val view = it.getChildAt(i) as EntryEditCustomField
val key = view.label
val value = view.value
val protect = view.isProtected
newEntry.addExtraField(key, ProtectedString(protect, value))
}
}
}
mDatabase?.stopManageEntry(newEntry)
}
/**
* Retrieve the icon by the selection, or the first icon in the list if the entry is new or the last one
*/
private fun retrieveIcon(): PwIcon {
return if (mSelectedIconStandard?.isUnknown != true)
mSelectedIconStandard
else {
if (mIsNew) {
mDatabase?.iconFactory?.keyIcon
} else {
// Keep previous icon, if no new one was selected
mEntry?.icon
}
} ?: PwIconStandard()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
inflater.inflate(R.menu.database_lock, menu)
MenuUtil.contributionMenuInflater(inflater, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_lock -> {
lockAndExit()
return true
}
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
}
private fun assignIconView() {
mEntry?.icon?.let {
mDatabase?.drawFactory?.assignDatabaseIconTo(
this,
entryIconView,
it,
iconColor)
}
}
private fun fillEntryDataInContentsView(entry: EntryVersioned) {
assignIconView()
// Don't start the field reference manager, we want to see the raw ref
mDatabase?.stopManageEntry(entry)
entryTitleView?.setText(entry.title)
entryUserNameView?.setText(entry.username)
entryUrlView?.setText(entry.url)
val password = entry.password
entryPasswordView?.setText(password)
entryConfirmationPasswordView?.setText(password)
entryCommentView?.setText(entry.notes)
val visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this)
if (visibilityFontActivated) {
Util.applyFontVisibilityTo(this, entryUserNameView)
Util.applyFontVisibilityTo(this, entryPasswordView)
Util.applyFontVisibilityTo(this, entryConfirmationPasswordView)
Util.applyFontVisibilityTo(this, entryCommentView)
}
if (entry.allowExtraFields()) {
val container = findViewById<LinearLayout>(R.id.entry_edit_advanced_container)
entry.fields.doActionToAllCustomProtectedField { key, value ->
val entryEditCustomField = EntryEditCustomField(this@EntryEditActivity)
entryEditCustomField.setData(key, value)
entryEditCustomField.setFontVisibility(visibilityFontActivated)
container.addView(entryEditCustomField)
}
}
}
override fun iconPicked(bundle: Bundle) {
mSelectedIconStandard = bundle.getParcelable(KEY_ICON_STANDARD)
mSelectedIconStandard?.let {
mEntry?.icon = it
}
assignIconView()
}
override fun onSaveInstanceState(outState: Bundle) {
if (!mSelectedIconStandard!!.isUnknown) {
outState.putParcelable(KEY_ICON_STANDARD, mSelectedIconStandard)
super.onSaveInstanceState(outState)
}
}
override fun acceptPassword(bundle: Bundle) {
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
entryPasswordView?.setText(it)
entryConfirmationPasswordView?.setText(it)
}
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
}
}
override fun cancelPassword(bundle: Bundle) {
// Do nothing here
}
override fun finish() {
// Assign entry callback as a result in all case
try {
mNewEntry?.let {
val bundle = Bundle()
val intentEntry = Intent()
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry)
intentEntry.putExtras(bundle)
if (mIsNew) {
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
} else {
setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry)
}
}
super.finish()
} catch (e: Exception) {
// Exception when parcelable can't be done
Log.e(TAG, "Cant add entry as result", e)
}
}
companion object {
private val TAG = EntryEditActivity::class.java.name
// Keys for current Activity
const val KEY_ENTRY = "entry"
const val KEY_PARENT = "parent"
// Keys for callback
const val ADD_ENTRY_RESULT_CODE = 31
const val UPDATE_ENTRY_RESULT_CODE = 32
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
const val UNKNOWN_MESSAGE = -1
/**
* Launch EntryEditActivity to update an existing entry
*
* @param activity from activity
* @param pwEntry Entry to update
*/
fun launch(activity: Activity, pwEntry: EntryVersioned) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, pwEntry.nodeId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
}
}
/**
* Launch EntryEditActivity to add a new entry
*
* @param activity from activity
* @param pwGroup Group who will contains new entry
*/
fun launch(activity: Activity, pwGroup: GroupVersioned) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, pwGroup.nodeId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
}
}
}
}

View File

@@ -0,0 +1,571 @@
/*
* 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
import android.Manifest
import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import android.support.v7.app.AlertDialog
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.dialogs.CreateFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.FileInformationDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.*
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.fileselect.DeleteFileHistoryAsyncTask
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
import com.kunzisoft.keepass.fileselect.OpenFileHistoryAsyncTask
import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.EmptyUtils
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import net.cachapa.expandablelayout.ExpandableLayout
import permissions.dispatcher.*
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.lang.ref.WeakReference
import java.net.URLDecoder
import java.util.*
@RuntimePermissions
class FileDatabaseSelectActivity : StylishActivity(),
CreateFileDialogFragment.DefinePathDialogListener,
AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
FileDatabaseHistoryAdapter.FileItemOpenListener,
FileDatabaseHistoryAdapter.FileSelectClearListener,
FileDatabaseHistoryAdapter.FileInformationShowListener {
// Views
private var fileListContainer: View? = null
private var createButtonView: View? = null
private var browseButtonView: View? = null
private var openButtonView: View? = null
private var fileSelectExpandableButtonView: View? = null
private var fileSelectExpandableLayout: ExpandableLayout? = null
private var openFileNameView: EditText? = null
// Adapter to manage database history list
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
private var mFileDatabaseHistory: FileDatabaseHistory? = null
private var mDatabaseFileUri: Uri? = null
private var mKeyFileHelper: KeyFileHelper? = null
private var mDefaultPath: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mFileDatabaseHistory = FileDatabaseHistory.getInstance(WeakReference(applicationContext))
setContentView(R.layout.file_selection)
fileListContainer = findViewById(R.id.container_file_list)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = ""
setSupportActionBar(toolbar)
openFileNameView = findViewById(R.id.file_filename)
// Set the initial value of the filename
mDefaultPath = (Environment.getExternalStorageDirectory().absolutePath
+ getString(R.string.database_file_path_default)
+ getString(R.string.database_file_name_default)
+ getString(R.string.database_file_extension_default))
openFileNameView?.setHint(R.string.open_link_database)
// Button to expand file selection
fileSelectExpandableButtonView = findViewById(R.id.file_select_expandable_button)
fileSelectExpandableLayout = findViewById(R.id.file_select_expandable)
fileSelectExpandableButtonView?.setOnClickListener { _ ->
if (fileSelectExpandableLayout?.isExpanded == true)
fileSelectExpandableLayout?.collapse()
else
fileSelectExpandableLayout?.expand()
}
// History list
val databaseFileListView = findViewById<RecyclerView>(R.id.file_list)
databaseFileListView.layoutManager = LinearLayoutManager(this)
// Open button
openButtonView = findViewById(R.id.open_database)
openButtonView?.setOnClickListener { _ ->
var fileName = openFileNameView?.text?.toString() ?: ""
mDefaultPath?.let {
if (fileName.isEmpty())
fileName = it
}
launchPasswordActivityWithPath(fileName)
}
// Create button
createButtonView = findViewById(R.id.create_database)
createButtonView?.setOnClickListener { openCreateFileDialogFragmentWithPermissionCheck() }
mKeyFileHelper = KeyFileHelper(this)
browseButtonView = findViewById(R.id.browse_button)
browseButtonView?.setOnClickListener(mKeyFileHelper!!.getOpenFileOnClickViewListener {
Uri.parse("file://" + openFileNameView!!.text.toString())
})
// Construct adapter with listeners
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this@FileDatabaseSelectActivity,
mFileDatabaseHistory?.databaseUriList ?: ArrayList())
mAdapterDatabaseHistory?.setOnItemClickListener(this)
mAdapterDatabaseHistory?.setFileSelectClearListener(this)
mAdapterDatabaseHistory?.setFileInformationShowListener(this)
databaseFileListView.adapter = mAdapterDatabaseHistory
// Load default database if not an orientation change
if (!(savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_STAY)
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "")
if (fileName!!.isNotEmpty()) {
val dbUri = UriUtil.parseDefaultFile(fileName)
var scheme: String? = null
if (dbUri != null)
scheme = dbUri.scheme
if (!EmptyUtils.isNullOrEmpty(scheme) && scheme!!.equals("file", ignoreCase = true)) {
val path = dbUri!!.path
val db = File(path!!)
if (db.exists()) {
launchPasswordActivityWithPath(path)
}
} else {
if (dbUri != null)
launchPasswordActivityWithPath(dbUri.toString())
}
}
}
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
}
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
// If no recent files
if (createButtonView != null
&& mFileDatabaseHistory != null
&& !mFileDatabaseHistory!!.hasRecentFiles() && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
createButtonView!!,
{
openCreateFileDialogFragmentWithPermissionCheck()
},
{
// But if the user cancel, it can also select a database
performedNextEducation(fileDatabaseSelectActivityEducation)
}))
else if (browseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
browseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mKeyFileHelper?.openFileOnClickViewListener?.onClick(it)
}
},
{
fileSelectExpandableButtonView?.let {
fileDatabaseSelectActivityEducation
.checkAndPerformedOpenLinkDatabaseEducation(it)
}
}
))
;
}
private fun fileNoFoundAction(e: FileNotFoundException) {
val error = getString(R.string.file_not_found_content)
Toast.makeText(this@FileDatabaseSelectActivity,
error, Toast.LENGTH_LONG).show()
Log.e(TAG, error, e)
}
private fun launchPasswordActivity(fileName: String, keyFile: String) {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
try {
PasswordActivity.launch(this@FileDatabaseSelectActivity,
fileName, keyFile)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
},
{
try {
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
fileName, keyFile)
finish()
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
fileName, keyFile,
assistStructure)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
}
})
}
private fun launchPasswordActivityWithPath(path: String) {
launchPasswordActivity(path, "")
// Delete flickering for kitkat <=
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
overridePendingTransition(0, 0)
}
private fun updateExternalStorageWarning() {
// To show errors
var warning = -1
val state = Environment.getExternalStorageState()
if (state == Environment.MEDIA_MOUNTED_READ_ONLY) {
warning = R.string.read_only_warning
} else if (state != Environment.MEDIA_MOUNTED) {
warning = R.string.warning_unmounted
}
val labelWarningView = findViewById<TextView>(R.id.label_warning)
if (warning != -1) {
labelWarningView.setText(warning)
labelWarningView.visibility = View.VISIBLE
} else {
labelWarningView.visibility = View.INVISIBLE
}
}
override fun onResume() {
super.onResume()
updateExternalStorageWarning()
updateFileListVisibility()
mAdapterDatabaseHistory!!.notifyDataSetChanged()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// only to keep the current activity
outState.putBoolean(EXTRA_STAY, true)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// NOTE: delegate the permission handling to generated method
onRequestPermissionsResult(requestCode, grantResults)
}
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
fun openCreateFileDialogFragment() {
val createFileDialogFragment = CreateFileDialogFragment()
createFileDialogFragment.show(supportFragmentManager, "createFileDialogFragment")
}
private fun updateFileListVisibility() {
if (mAdapterDatabaseHistory?.itemCount == 0)
fileListContainer?.visibility = View.INVISIBLE
else
fileListContainer?.visibility = View.VISIBLE
}
/**
* Create file for database
* @return If not created, return false
*/
private fun createDatabaseFile(path: Uri): Boolean {
val pathString = URLDecoder.decode(path.path)
// Make sure file name exists
if (pathString.isEmpty()) {
Log.e(TAG, getString(R.string.error_filename_required))
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_filename_required,
Toast.LENGTH_LONG).show()
return false
}
// Try to create the file
val file = File(pathString)
try {
if (file.exists()) {
Log.e(TAG, getString(R.string.error_database_exists) + " " + file)
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_database_exists,
Toast.LENGTH_LONG).show()
return false
}
val parent = file.parentFile
if (parent == null || parent.exists() && !parent.isDirectory) {
Log.e(TAG, getString(R.string.error_invalid_path) + " " + file)
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_invalid_path,
Toast.LENGTH_LONG).show()
return false
}
if (!parent.exists()) {
// Create parent directory
if (!parent.mkdirs()) {
Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + parent)
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_could_not_create_parent,
Toast.LENGTH_LONG).show()
return false
}
}
return file.createNewFile()
} catch (e: IOException) {
Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + e.localizedMessage)
e.printStackTrace()
Toast.makeText(
this@FileDatabaseSelectActivity,
getText(R.string.error_file_not_create).toString() + " "
+ e.localizedMessage,
Toast.LENGTH_LONG).show()
return false
}
}
override fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean {
mDatabaseFileUri = pathFile
if (pathFile == null)
return false
return if (createDatabaseFile(pathFile)) {
AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog")
true
} else
false
}
override fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean {
return true
}
override fun onAssignKeyDialogPositiveClick(
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
try {
mDatabaseFileUri?.path?.let { databaseFilename ->
// Create the new database and start prof
ProgressDialogThread(this@FileDatabaseSelectActivity,
{
CreateDatabaseRunnable(databaseFilename) { database ->
// TODO store database created
AssignPasswordInDatabaseRunnable(
this@FileDatabaseSelectActivity,
database,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile,
true, // TODO get readonly
LaunchGroupActivityFinish(UriUtil.parseDefaultFile(databaseFilename))
)
}
},
R.string.progress_create)
.start()
}
} catch (e: Exception) {
val error = "Unable to create database with this password and key file"
Toast.makeText(this, error, Toast.LENGTH_LONG).show()
Log.e(TAG, error + " " + e.message)
// TODO remove
e.printStackTrace()
}
}
private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() {
override fun run() {
finishRun(true, null)
}
override fun onFinishRun(isSuccess: Boolean, message: String?) {
runOnUiThread {
if (isSuccess) {
// Add database to recent files
mFileDatabaseHistory?.addDatabaseUri(fileURI)
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
GroupActivity.launch(this@FileDatabaseSelectActivity)
} else {
Log.e(TAG, "Unable to open the database")
}
}
}
}
override fun onAssignKeyDialogNegativeClick(
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
}
override fun onFileItemOpenListener(itemPosition: Int) {
OpenFileHistoryAsyncTask({ fileName, keyFile ->
if (fileName != null && keyFile != null)
launchPasswordActivity(fileName, keyFile)
updateFileListVisibility()
}, mFileDatabaseHistory).execute(itemPosition)
}
override fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel) {
FileInformationDialogFragment.newInstance(fileDatabaseModel).show(supportFragmentManager, "fileInformation")
}
override fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean {
DeleteFileHistoryAsyncTask({
fileDatabaseModel.fileUri?.let {
mFileDatabaseHistory?.deleteDatabaseUri(it)
}
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
}, mFileDatabaseHistory, mAdapterDatabaseHistory).execute(fileDatabaseModel)
return true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) {
launchPasswordActivityWithPath(uri.toString())
} else {
fileSelectExpandableLayout?.expand(false)
openFileNameView?.setText(uri.toString())
}
}
}
}
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showRationaleForExternalStorage(request: PermissionRequest) {
AlertDialog.Builder(this)
.setMessage(R.string.permission_external_storage_rationale_write_database)
.setPositiveButton(R.string.allow) { _, _ -> request.proceed() }
.setNegativeButton(R.string.cancel) { _, _ -> request.cancel() }
.show()
}
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showDeniedForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show()
}
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showNeverAskForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
MenuUtil.defaultMenuInflater(menuInflater, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
}
companion object {
private const val TAG = "FileDbSelectActivity"
private const val EXTRA_STAY = "EXTRA_STAY"
/*
* -------------------------
* No Standard Launch, pass by PasswordActivity
* -------------------------
*/
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
fun launchForKeyboardSelection(activity: Activity) {
KeyboardHelper.startActivityForKeyboardSelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure) {
AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
assistStructure)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
package com.kunzisoft.keepass.activities;
import android.content.Intent;
public interface IntentBuildLauncher {
void launchActivity(Intent intent);
}

View File

@@ -1,294 +0,0 @@
package com.kunzisoft.keepass.activities;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.adapters.NodeAdapter;
import com.kunzisoft.keepass.database.SortNodeEnum;
import com.kunzisoft.keepass.database.element.GroupVersioned;
import com.kunzisoft.keepass.database.element.NodeVersioned;
import com.kunzisoft.keepass.dialogs.SortDialogFragment;
import com.kunzisoft.keepass.settings.PreferencesUtil;
import com.kunzisoft.keepass.stylish.StylishFragment;
public class ListNodesFragment extends StylishFragment implements
SortDialogFragment.SortSelectionListener {
private static final String TAG = ListNodesFragment.class.getName();
private static final String GROUP_KEY = "GROUP_KEY";
private static final String IS_SEARCH = "IS_SEARCH";
private NodeAdapter.NodeClickCallback nodeClickCallback;
private NodeAdapter.NodeMenuListener nodeMenuListener;
private OnScrollListener onScrollListener;
private RecyclerView listView;
private GroupVersioned currentGroup;
private NodeAdapter mAdapter;
private View notFoundView;
private boolean isASearchResult;
// Preferences for sorting
private SharedPreferences prefs;
private boolean readOnly;
public static ListNodesFragment newInstance(GroupVersioned group, boolean readOnly, boolean isASearch) {
Bundle bundle = new Bundle();
if (group != null) {
bundle.putParcelable(GROUP_KEY, group);
}
bundle.putBoolean(IS_SEARCH, isASearch);
ReadOnlyHelper.INSTANCE.putReadOnlyInBundle(bundle, readOnly);
ListNodesFragment listNodesFragment = new ListNodesFragment();
listNodesFragment.setArguments(bundle);
return listNodesFragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
nodeClickCallback = (NodeAdapter.NodeClickCallback) context;
} catch (ClassCastException e) {
// The activity doesn't implement the interface, throw exception
throw new ClassCastException(context.toString()
+ " must implement " + NodeAdapter.NodeClickCallback.class.getName());
}
try {
nodeMenuListener = (NodeAdapter.NodeMenuListener) context;
} catch (ClassCastException e) {
nodeMenuListener = null;
// Context menu can be omit
Log.w(TAG, context.toString()
+ " must implement " + NodeAdapter.NodeMenuListener.class.getName());
}
try {
onScrollListener = (OnScrollListener) context;
} catch (ClassCastException e) {
onScrollListener = null;
// Context menu can be omit
Log.w(TAG, context.toString()
+ " must implement " + RecyclerView.OnScrollListener.class.getName());
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if ( getActivity() != null ) {
setHasOptionsMenu(true);
readOnly = ReadOnlyHelper.INSTANCE.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, getArguments());
if (getArguments() != null) {
// Contains all the group in element
if (getArguments().containsKey(GROUP_KEY)) {
currentGroup = getArguments().getParcelable(GROUP_KEY);
}
if (getArguments().containsKey(IS_SEARCH)) {
isASearchResult = getArguments().getBoolean(IS_SEARCH);
}
}
mAdapter = new NodeAdapter(getContextThemed(), getActivity().getMenuInflater());
mAdapter.setReadOnly(readOnly);
mAdapter.setIsASearchResult(isASearchResult);
mAdapter.setOnNodeClickListener(nodeClickCallback);
if (nodeMenuListener != null) {
mAdapter.setActivateContextMenu(true);
mAdapter.setNodeMenuListener(nodeMenuListener);
}
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
ReadOnlyHelper.INSTANCE.onSaveInstanceState(outState, readOnly);
super.onSaveInstanceState(outState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
// To apply theme
View rootView = inflater.cloneInContext(getContextThemed())
.inflate(R.layout.list_nodes_fragment, container, false);
listView = rootView.findViewById(R.id.nodes_list);
notFoundView = rootView.findViewById(R.id.not_found_container);
if (onScrollListener != null) {
listView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
onScrollListener.onScrolled(dy);
}
});
}
return rootView;
}
@Override
public void onResume() {
super.onResume();
rebuildList();
if (isASearchResult && mAdapter.isEmpty()) {
// To show the " no search entry found "
listView.setVisibility(View.GONE);
notFoundView.setVisibility(View.VISIBLE);
} else {
listView.setVisibility(View.VISIBLE);
notFoundView.setVisibility(View.GONE);
}
}
public void rebuildList() {
// Add elements to the list
if (currentGroup != null)
mAdapter.rebuildList(currentGroup);
assignListToNodeAdapter(listView);
}
protected void assignListToNodeAdapter(RecyclerView recyclerView) {
recyclerView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(mAdapter);
}
@Override
public void onSortSelected(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore, boolean recycleBinBottom) {
// Toggle setting
SharedPreferences.Editor editor = prefs.edit();
editor.putString(getString(R.string.sort_node_key), sortNodeEnum.name());
editor.putBoolean(getString(R.string.sort_ascending_key), ascending);
editor.putBoolean(getString(R.string.sort_group_before_key), groupsBefore);
editor.putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom);
editor.apply();
// Tell the adapter to refresh it's list
mAdapter.notifyChangeSort(sortNodeEnum, ascending, groupsBefore);
mAdapter.rebuildList(currentGroup);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.tree, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ) {
case R.id.menu_sort:
SortDialogFragment sortDialogFragment;
/*
// TODO Recycle bin bottom
if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) {
sortDialogFragment =
SortDialogFragment.getInstance(
PrefsUtil.getListSort(this),
PrefsUtil.getAscendingSort(this),
PrefsUtil.getGroupsBeforeSort(this),
PrefsUtil.getRecycleBinBottomSort(this));
} else {
*/
sortDialogFragment =
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(getContext()),
PreferencesUtil.getAscendingSort(getContext()),
PreferencesUtil.getGroupsBeforeSort(getContext()));
//}
sortDialogFragment.show(getChildFragmentManager(), "sortDialog");
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE:
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE ||
resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
NodeVersioned newNode = data.getParcelableExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY);
if (newNode != null) {
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
mAdapter.addNode(newNode);
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
//mAdapter.updateLastNodeRegister(newNode);
mAdapter.rebuildList(currentGroup);
}
} else {
Log.e(this.getClass().getName(), "New node can be retrieve in Activity Result");
}
}
break;
}
}
public boolean isEmpty() {
return mAdapter == null || mAdapter.getItemCount() <= 0;
}
public void addNode(NodeVersioned newNode) {
mAdapter.addNode(newNode);
}
public void updateNode(NodeVersioned oldNode, NodeVersioned newNode) {
mAdapter.updateNode(oldNode, newNode);
}
public void removeNode(NodeVersioned pwNode) {
mAdapter.removeNode(pwNode);
}
public GroupVersioned getMainGroup() {
return currentGroup;
}
public interface OnScrollListener {
/**
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
* called after the scroll has completed.
*
* @param dy The amount of vertical scroll.
*/
void onScrolled(int dy);
}
}

View File

@@ -0,0 +1,280 @@
package com.kunzisoft.keepass.activities
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.element.GroupVersioned
import com.kunzisoft.keepass.database.element.NodeVersioned
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
private var nodeClickCallback: NodeAdapter.NodeClickCallback? = null
private var nodeMenuListener: NodeAdapter.NodeMenuListener? = null
private var onScrollListener: OnScrollListener? = null
private var listView: RecyclerView? = null
var mainGroup: GroupVersioned? = null
private set
private var mAdapter: NodeAdapter? = null
private var notFoundView: View? = null
private var isASearchResult: Boolean = false
// Preferences for sorting
private var prefs: SharedPreferences? = null
private var readOnly: Boolean = false
val isEmpty: Boolean
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
override fun onAttach(context: Context?) {
super.onAttach(context)
try {
nodeClickCallback = context as NodeAdapter.NodeClickCallback?
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context?.toString()
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
}
try {
nodeMenuListener = context as NodeAdapter.NodeMenuListener?
} catch (e: ClassCastException) {
nodeMenuListener = null
// Context menu can be omit
Log.w(TAG, context?.toString()
+ " must implement " + NodeAdapter.NodeMenuListener::class.java.name)
}
try {
onScrollListener = context as OnScrollListener?
} catch (e: ClassCastException) {
onScrollListener = null
// Context menu can be omit
Log.w(TAG, context?.toString()
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.let { currentActivity ->
setHasOptionsMenu(true)
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
arguments?.let { args ->
// Contains all the group in element
if (args.containsKey(GROUP_KEY)) {
mainGroup = args.getParcelable(GROUP_KEY)
}
if (args.containsKey(IS_SEARCH)) {
isASearchResult = args.getBoolean(IS_SEARCH)
}
}
mAdapter = NodeAdapter(getContextThemed(), currentActivity.menuInflater)
mAdapter?.apply {
setReadOnly(readOnly)
setIsASearchResult(isASearchResult)
setOnNodeClickListener(nodeClickCallback)
setActivateContextMenu(true)
setNodeMenuListener(nodeMenuListener)
}
prefs = PreferenceManager.getDefaultSharedPreferences(context)
}
}
override fun onSaveInstanceState(outState: Bundle) {
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
super.onSaveInstanceState(outState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
// To apply theme
val rootView = inflater.cloneInContext(getContextThemed())
.inflate(R.layout.list_nodes_fragment, container, false)
listView = rootView.findViewById(R.id.nodes_list)
notFoundView = rootView.findViewById(R.id.not_found_container)
onScrollListener?.let { onScrollListener ->
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
onScrollListener.onScrolled(dy)
}
})
}
return rootView
}
override fun onResume() {
super.onResume()
rebuildList()
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
listView?.visibility = View.GONE
notFoundView?.visibility = View.VISIBLE
} else {
listView?.visibility = View.VISIBLE
notFoundView?.visibility = View.GONE
}
}
fun rebuildList() {
// Add elements to the list
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) {
// Toggle setting
prefs?.edit()?.apply {
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
putBoolean(getString(R.string.sort_ascending_key), ascending)
putBoolean(getString(R.string.sort_group_before_key), groupsBefore)
putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom)
apply()
}
// Tell the adapter to refresh it's list
mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore)
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.tree, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_sort -> {
val sortDialogFragment: SortDialogFragment
/*
// TODO Recycle bin bottom
if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) {
sortDialogFragment =
SortDialogFragment.getInstance(
PrefsUtil.getListSort(this),
PrefsUtil.getAscendingSort(this),
PrefsUtil.getGroupsBeforeSort(this),
PrefsUtil.getRecycleBinBottomSort(this));
} else {
*/
sortDialogFragment = SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context))
//}
sortDialogFragment.show(childFragmentManager, "sortDialog")
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<NodeVersioned>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
mAdapter?.addNode(newNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
//mAdapter.updateLastNodeRegister(newNode);
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
}
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
}
}
}
}
fun addNode(newNode: NodeVersioned) {
mAdapter?.addNode(newNode)
}
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
mAdapter?.updateNode(oldNode, newNode)
}
fun removeNode(pwNode: NodeVersioned) {
mAdapter?.removeNode(pwNode)
}
interface OnScrollListener {
/**
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
* called after the scroll has completed.
*
* @param dy The amount of vertical scroll.
*/
fun onScrolled(dy: Int)
}
companion object {
private val TAG = ListNodesFragment::class.java.name
private const val GROUP_KEY = "GROUP_KEY"
private const val IS_SEARCH = "IS_SEARCH"
fun newInstance(group: GroupVersioned?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
val bundle = Bundle()
if (group != null) {
bundle.putParcelable(GROUP_KEY, group)
}
bundle.putBoolean(IS_SEARCH, isASearch)
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly)
val listNodesFragment = ListNodesFragment()
listNodesFragment.arguments = bundle
return listNodesFragment
}
}
}

View File

@@ -0,0 +1,892 @@
/*
* 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
import android.Manifest
import android.app.Activity
import android.app.assist.AssistStructure
import android.app.backup.BackupManager
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.hardware.fingerprint.FingerprintManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import android.support.v7.app.AlertDialog
import android.support.v7.widget.Toolbar
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogHelper
import com.kunzisoft.keepass.activities.helpers.*
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.fingerprint.FingerPrintAnimatedVector
import com.kunzisoft.keepass.fingerprint.FingerPrintExplanationDialog
import com.kunzisoft.keepass.fingerprint.FingerPrintHelper
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.EmptyUtils
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import permissions.dispatcher.*
import java.io.File
import java.io.FileNotFoundException
import java.lang.ref.WeakReference
@RuntimePermissions
class PasswordActivity : StylishActivity(),
FingerPrintHelper.FingerPrintCallback,
UriIntentInitTaskCallback {
// Views
private var toolbar: Toolbar? = null
private var fingerprintContainerView: View? = null
private var fingerPrintAnimatedVector: FingerPrintAnimatedVector? = null
private var fingerprintTextView: TextView? = null
private var fingerprintImageView: ImageView? = null
private var filenameView: TextView? = null
private var passwordView: EditText? = null
private var keyFileView: EditText? = null
private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null
private var checkboxDefaultDatabaseView: CompoundButton? = null
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private var mDatabaseFileUri: Uri? = null
private var prefs: SharedPreferences? = null
private var prefsNoBackup: SharedPreferences? = null
private var mRememberKeyFile: Boolean = false
private var mKeyFileHelper: KeyFileHelper? = null
private var readOnly: Boolean = false
private var fingerPrintHelper: FingerPrintHelper? = null
private var fingerprintMustBeConfigured = true
private var fingerPrintMode: FingerPrintHelper.Mode? = null
// makes it possible to store passwords per database
private val preferenceKeyValue: String
get() = PREF_KEY_VALUE_PREFIX + (mDatabaseFileUri?.path ?: "")
private val preferenceKeyIvSpec: String
get() = PREF_KEY_IV_PREFIX + (mDatabaseFileUri?.path ?: "")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefsNoBackup = PreferencesUtil.getNoBackupSharedPreferences(applicationContext)
mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key),
resources.getBoolean(R.bool.keyfile_default))
setContentView(R.layout.password)
toolbar = findViewById(R.id.toolbar)
toolbar?.title = getString(R.string.app_name)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
confirmButtonView = findViewById(R.id.pass_ok)
filenameView = findViewById(R.id.filename)
passwordView = findViewById(R.id.password)
keyFileView = findViewById(R.id.pass_keyfile)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
val browseView = findViewById<View>(R.id.browse_button)
mKeyFileHelper = KeyFileHelper(this@PasswordActivity)
browseView.setOnClickListener(mKeyFileHelper!!.openFileOnClickViewListener)
passwordView?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
checkboxPasswordView?.isChecked = true
}
})
keyFileView?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (editable.toString().isNotEmpty() && checkboxKeyFileView?.isChecked != true)
checkboxKeyFileView?.isChecked = true
}
})
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerprintContainerView = findViewById(R.id.fingerprint_container)
fingerprintTextView = findViewById(R.id.fingerprint_label)
fingerprintImageView = findViewById(R.id.fingerprint_image)
initForFingerprint()
// Init the fingerprint animation
fingerPrintAnimatedVector = FingerPrintAnimatedVector(this,
fingerprintImageView!!)
}
}
override fun onResume() {
// If the database isn't accessible make sure to clear the password field, if it
// was saved in the instance state
if (App.currentDatabase.loaded) {
setEmptyViews()
}
// For check shutdown
super.onResume()
// Enable or not the open button
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
checkboxPasswordView?.let {
confirmButtonView?.isEnabled = it.isChecked
}
} else {
confirmButtonView?.isEnabled = true
}
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
confirmButtonView?.isEnabled = isChecked
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (fingerPrintHelper == null || !fingerPrintHelper!!.isFingerprintInitialized) {
initForFingerprint()
}
// Start the animation in all cases
fingerPrintAnimatedVector?.startScan()
} else {
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
}
UriIntentInitTask(WeakReference(this), this, mRememberKeyFile)
.execute(intent)
}
override fun onSaveInstanceState(outState: Bundle) {
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
super.onSaveInstanceState(outState)
}
override fun onPostInitTask(dbUri: Uri?, keyFileUri: Uri?, errorStringId: Int?) {
mDatabaseFileUri = dbUri
if (errorStringId != null) {
Toast.makeText(this@PasswordActivity, errorStringId, Toast.LENGTH_LONG).show()
finish()
return
}
// Verify permission to read file
if (mDatabaseFileUri != null && !mDatabaseFileUri!!.scheme!!.contains("content"))
doNothingWithPermissionCheck()
// Define title
val dbUriString = mDatabaseFileUri?.toString() ?: ""
if (dbUriString.isNotEmpty()) {
if (PreferencesUtil.isFullFilePathEnable(this))
filenameView?.text = dbUriString
else
filenameView?.text = File(mDatabaseFileUri!!.path!!).name // TODO Encapsulate
}
// Define Key File text
val keyUriString = keyFileUri?.toString() ?: ""
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18
populateKeyFileTextView(keyUriString)
}
// Define listeners for default database checkbox and validate button
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
var newDefaultFileName = ""
if (isChecked) {
newDefaultFileName = mDatabaseFileUri?.toString() ?: newDefaultFileName
}
prefs?.edit()?.apply() {
putString(KEY_DEFAULT_FILENAME, newDefaultFileName)
apply()
}
val backupManager = BackupManager(this@PasswordActivity)
backupManager.dataChanged()
}
confirmButtonView?.setOnClickListener { verifyAllViewsAndLoadDatabase() }
// Retrieve settings for default database
val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "")
if (mDatabaseFileUri != null
&& !EmptyUtils.isNullOrEmpty(mDatabaseFileUri!!.path)
&& UriUtil.equalsDefaultfile(mDatabaseFileUri, defaultFilename)) {
checkboxDefaultDatabaseView?.isChecked = true
}
// checks if fingerprint is available, will also start listening for fingerprints when available
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
checkFingerprintAvailability()
}
// If Activity is launch with a password and want to open directly
val intent = intent
val password = intent.getStringExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) {
populatePasswordTextView(password)
}
if (launchImmediately) {
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
}
}
private fun setEmptyViews() {
populatePasswordTextView(null)
// Bug KeepassDX #18
if (!mRememberKeyFile) {
populateKeyFileTextView(null)
}
}
private fun populatePasswordTextView(text: String?) {
if (text == null || text.isEmpty()) {
passwordView?.setText("")
if (checkboxPasswordView?.isChecked == true)
checkboxPasswordView?.isChecked = false
} else {
passwordView?.setText(text)
if (checkboxPasswordView?.isChecked != true)
checkboxPasswordView?.isChecked = true
}
}
private fun populateKeyFileTextView(text: String?) {
if (text == null || text.isEmpty()) {
keyFileView?.setText("")
if (checkboxKeyFileView?.isChecked == true)
checkboxKeyFileView?.isChecked = false
} else {
keyFileView?.setText(text)
if (checkboxKeyFileView?.isChecked != true)
checkboxKeyFileView?.isChecked = true
}
}
// fingerprint related code here
@RequiresApi(api = Build.VERSION_CODES.M)
private fun initForFingerprint() {
fingerPrintMode = FingerPrintHelper.Mode.NOT_CONFIGURED_MODE
fingerPrintHelper = FingerPrintHelper(this, this)
checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
if (!fingerprintMustBeConfigured) {
// encrypt or decrypt mode based on how much input or not
if (checked) {
toggleFingerprintMode(FingerPrintHelper.Mode.STORE_MODE)
} else {
if (prefsNoBackup?.contains(preferenceKeyValue) == true) {
toggleFingerprintMode(FingerPrintHelper.Mode.OPEN_MODE)
} else {
// This happens when no fingerprints are registered.
toggleFingerprintMode(FingerPrintHelper.Mode.WAITING_PASSWORD_MODE)
}
}
}
// Add old listener to enable the button, only be call here because of onCheckedChange bug
enableButtonOnCheckedChangeListener?.onCheckedChanged(compoundButton, checked)
}
// callback for fingerprint findings
fingerPrintHelper?.setAuthenticationCallback(object : FingerprintManager.AuthenticationCallback() {
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence) {
when (errorCode) {
5 -> Log.i(TAG, "Fingerprint authentication error. Code : $errorCode Error : $errString")
else -> {
Log.e(TAG, "Fingerprint authentication error. Code : $errorCode Error : $errString")
setFingerPrintView(errString.toString(), true)
}
}
}
override fun onAuthenticationHelp(
helpCode: Int,
helpString: CharSequence) {
Log.w(TAG, "Fingerprint authentication help. Code : $helpCode Help : $helpString")
showError(helpString)
setFingerPrintView(helpString.toString(), true)
fingerprintTextView?.text = helpString
}
override fun onAuthenticationFailed() {
Log.e(TAG, "Fingerprint authentication failed, fingerprint not recognized")
showError(R.string.fingerprint_not_recognized)
}
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
when (fingerPrintMode) {
FingerPrintHelper.Mode.STORE_MODE -> {
// newly store the entered password in encrypted way
fingerPrintHelper?.encryptData(passwordView?.text.toString())
}
FingerPrintHelper.Mode.OPEN_MODE -> {
// retrieve the encrypted value from preferences
prefsNoBackup?.getString(preferenceKeyValue, null)?.let {
fingerPrintHelper?.decryptData(it)
}
}
}
}
})
}
@RequiresApi(api = Build.VERSION_CODES.M)
private fun initEncryptData() {
setFingerPrintView(R.string.store_with_fingerprint)
fingerPrintMode = FingerPrintHelper.Mode.STORE_MODE
fingerPrintHelper?.initEncryptData()
}
@RequiresApi(api = Build.VERSION_CODES.M)
private fun initDecryptData() {
setFingerPrintView(R.string.scanning_fingerprint)
fingerPrintMode = FingerPrintHelper.Mode.OPEN_MODE
if (fingerPrintHelper != null) {
prefsNoBackup?.getString(preferenceKeyIvSpec, null)?.let {
fingerPrintHelper?.initDecryptData(it)
}
}
}
@RequiresApi(api = Build.VERSION_CODES.M)
private fun initWaitData() {
setFingerPrintView(R.string.no_password_stored, true)
fingerPrintMode = FingerPrintHelper.Mode.WAITING_PASSWORD_MODE
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Synchronized
private fun toggleFingerprintMode(newMode: FingerPrintHelper.Mode) {
when (newMode) {
FingerPrintHelper.Mode.WAITING_PASSWORD_MODE -> setFingerPrintView(R.string.no_password_stored, true)
FingerPrintHelper.Mode.STORE_MODE -> setFingerPrintView(R.string.store_with_fingerprint)
FingerPrintHelper.Mode.OPEN_MODE -> setFingerPrintView(R.string.scanning_fingerprint)
else -> {}
}
if (newMode != fingerPrintMode) {
fingerPrintMode = newMode
reInitWithFingerprintMode()
}
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Synchronized
private fun reInitWithFingerprintMode() {
when (fingerPrintMode) {
FingerPrintHelper.Mode.STORE_MODE -> initEncryptData()
FingerPrintHelper.Mode.WAITING_PASSWORD_MODE -> initWaitData()
FingerPrintHelper.Mode.OPEN_MODE -> initDecryptData()
else -> {}
}
// Show fingerprint key deletion
invalidateOptionsMenu()
}
override fun onPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerPrintAnimatedVector?.stopScan()
// stop listening when we go in background
fingerPrintMode = FingerPrintHelper.Mode.NOT_CONFIGURED_MODE
fingerPrintHelper?.stopListening()
}
super.onPause()
}
private fun setFingerPrintVisibility(vis: Int) {
runOnUiThread { fingerprintContainerView?.visibility = vis }
}
private fun setFingerPrintView(textId: Int, lock: Boolean = false) {
setFingerPrintView(getString(textId), lock)
}
private fun setFingerPrintView(text: CharSequence, lock: Boolean) {
runOnUiThread {
fingerprintContainerView?.alpha = if (lock) 0.8f else 1f
fingerprintTextView?.text = text
}
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Synchronized
private fun checkFingerprintAvailability() {
// fingerprint not supported (by API level or hardware) so keep option hidden
// or manually disable
if (!PreferencesUtil.isFingerprintEnable(applicationContext)
|| !FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager::class.java))) {
setFingerPrintVisibility(View.GONE)
} else {
// show explanations
fingerprintContainerView?.setOnClickListener { _ ->
FingerPrintExplanationDialog().show(supportFragmentManager, "fingerprintDialog")
}
setFingerPrintVisibility(View.VISIBLE)
if (fingerPrintHelper?.hasEnrolledFingerprints() != true) {
// This happens when no fingerprints are registered. Listening won't start
setFingerPrintView(R.string.configure_fingerprint, true)
} else {
fingerprintMustBeConfigured = false
// fingerprint available but no stored password found yet for this DB so show info don't listen
if (prefsNoBackup?.contains(preferenceKeyValue) != true) {
if (checkboxPasswordView?.isChecked == true) {
// listen for encryption
initEncryptData()
} else {
// wait for typing
initWaitData()
}
} else {
// listen for decryption
initDecryptData()
}// all is set here so we can confirm to user and start listening for fingerprints
}// finally fingerprint available and configured so we can use it
}// fingerprint is available but not configured show icon but in disabled state with some information
// Show fingerprint key deletion
invalidateOptionsMenu()
}
private fun removePrefsNoBackupKey() {
prefsNoBackup?.edit()
?.remove(preferenceKeyValue)
?.remove(preferenceKeyIvSpec)
?.apply()
}
override fun handleEncryptedResult(
value: String,
ivSpec: String) {
prefsNoBackup?.edit()
?.putString(preferenceKeyValue, value)
?.putString(preferenceKeyIvSpec, ivSpec)
?.apply()
verifyAllViewsAndLoadDatabase()
setFingerPrintView(R.string.encrypted_value_stored)
}
override fun handleDecryptedResult(passwordValue: String) {
// Load database directly
verifyKeyFileViewsAndLoadDatabase(passwordValue)
}
@RequiresApi(api = Build.VERSION_CODES.M)
override fun onInvalidKeyException(e: Exception) {
showError(getString(R.string.fingerprint_invalid_key))
deleteEntryKey()
}
@RequiresApi(api = Build.VERSION_CODES.M)
override fun onFingerPrintException(e: Exception) {
// Don't show error here;
// showError(getString(R.string.fingerprint_error, e.getMessage()));
// Can be uninit in Activity and init in fragment
setFingerPrintView(e.localizedMessage, true)
}
@RequiresApi(api = Build.VERSION_CODES.M)
private fun deleteEntryKey() {
fingerPrintHelper?.deleteEntryKey()
removePrefsNoBackupKey()
fingerPrintMode = FingerPrintHelper.Mode.NOT_CONFIGURED_MODE
checkFingerprintAvailability()
}
private fun showError(messageId: Int) {
showError(getString(messageId))
}
private fun showError(message: CharSequence) {
runOnUiThread { Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show() }
}
private fun verifyAllViewsAndLoadDatabase() {
verifyCheckboxesAndLoadDatabase(
passwordView?.text.toString(),
UriUtil.parseDefaultFile(keyFileView?.text.toString()))
}
private fun verifyCheckboxesAndLoadDatabase(password: String?, keyFile: Uri?) {
var pass = password
var keyF = keyFile
if (checkboxPasswordView?.isChecked != true) {
pass = null
}
if (checkboxKeyFileView?.isChecked != true) {
keyF = null
}
loadDatabase(pass, keyF)
}
private fun verifyKeyFileViewsAndLoadDatabase(password: String) {
val key = keyFileView?.text.toString()
var keyUri = UriUtil.parseDefaultFile(key)
if (checkboxKeyFileView?.isChecked != true) {
keyUri = null
}
loadDatabase(password, keyUri)
}
private fun loadDatabase(password: String?, keyFile: Uri?) {
// Clear before we load
val database = App.currentDatabase
database.closeAndClear(applicationContext)
mDatabaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database
ProgressDialogThread(this,
{ progressTaskUpdater ->
LoadDatabaseRunnable(
WeakReference(this@PasswordActivity.applicationContext),
database,
databaseUri,
password,
keyFile,
progressTaskUpdater,
AfterLoadingDatabase(database))
},
R.string.loading_database).start()
}
}
/**
* Called after verify and try to opening the database
*/
private inner class AfterLoadingDatabase internal constructor(var database: Database) : ActionRunnable() {
override fun onFinishRun(isSuccess: Boolean, message: String?) {
runOnUiThread {
// Recheck fingerprint if error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Stay with the same mode
reInitWithFingerprintMode()
}
if (database.isPasswordEncodingError) {
val dialog = PasswordEncodingDialogHelper()
dialog.show(this@PasswordActivity,
DialogInterface.OnClickListener { _, _ -> launchGroupActivity() })
} else if (isSuccess) {
launchGroupActivity()
} else {
if (message != null && message.isNotEmpty()) {
Toast.makeText(this@PasswordActivity, message, Toast.LENGTH_LONG).show()
}
}
}
}
}
private fun launchGroupActivity() {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
GroupActivity.launch(this@PasswordActivity, readOnly)
},
{
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly)
// Do not keep history
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
}
})
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
// Read menu
inflater.inflate(R.menu.open_file, menu)
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
MenuUtil.defaultMenuInflater(inflater, menu)
// Fingerprint menu
if (!fingerprintMustBeConfigured && prefsNoBackup?.contains(preferenceKeyValue) == true)
inflater.inflate(R.menu.fingerprint, menu)
super.onCreateOptionsMenu(menu)
// Show education views
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
return true
}
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
menu: Menu) {
if (toolbar != null
&& passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation(
toolbar!!,
{
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
}))
else if (toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
toolbar!!.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)
}))
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& PreferencesUtil.isFingerprintEnable(applicationContext)
&& FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager::class.java))
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(fingerprintImageView!!))
;
}
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
if (readOnly) {
togglePassword.setTitle(R.string.menu_file_selection_read_only)
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
} else {
togglePassword.setTitle(R.string.menu_open_file_read_and_write)
togglePassword.setIcon(R.drawable.ic_read_write_white_24dp)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> finish()
R.id.menu_open_file_read_mode_key -> {
readOnly = !readOnly
changeOpenFileReadIcon(item)
}
R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEntryKey()
}
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
}
return super.onOptionsItemSelected(item)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// NOTE: delegate the permission handling to generated method
onRequestPermissionsResult(requestCode, grantResults)
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
var keyFileResult = false
mKeyFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
populateKeyFileTextView(uri.toString())
}
}
}
if (!keyFileResult) {
// this block if not a key file response
when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
setEmptyViews()
App.currentDatabase.closeAndClear(applicationContext)
}
}
}
}
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
fun doNothing() {
}
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showRationaleForExternalStorage(request: PermissionRequest) {
AlertDialog.Builder(this)
.setMessage(R.string.permission_external_storage_rationale_read_database)
.setPositiveButton(R.string.allow) { _, _ -> request.proceed() }
.setNegativeButton(R.string.cancel) { _, _ -> request.cancel() }
.show()
}
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showDeniedForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show()
finish()
}
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showNeverAskForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show()
finish()
}
companion object {
private val TAG = PasswordActivity::class.java.name
const val KEY_DEFAULT_FILENAME = "defaultFileName"
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private const val PREF_KEY_VALUE_PREFIX = "valueFor_" // key is a combination of db file name and this prefix
private const val PREF_KEY_IV_PREFIX = "ivFor_" // key is a combination of db file name and this prefix
private fun buildAndLaunchIntent(activity: Activity, fileName: String, keyFile: String,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, PasswordActivity::class.java)
intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName)
intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile)
intentBuildLauncher.invoke(intent)
}
@Throws(FileNotFoundException::class)
private fun verifyFileNameUriFromLaunch(fileName: String) {
if (EmptyUtils.isNullOrEmpty(fileName)) {
throw FileNotFoundException()
}
val uri = UriUtil.parseDefaultFile(fileName)
val scheme = uri.scheme
if (!EmptyUtils.isNullOrEmpty(scheme) && scheme.equals("file", ignoreCase = true)) {
val dbFile = File(uri.path!!)
if (!dbFile.exists()) {
throw FileNotFoundException()
}
}
}
/*
* -------------------------
* Standard Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launch(
activity: Activity,
fileName: String,
keyFile: String) {
verifyFileNameUriFromLaunch(fileName)
buildAndLaunchIntent(activity, fileName, keyFile) { activity.startActivity(it) }
}
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForKeyboardResult(
activity: Activity,
fileName: String,
keyFile: String) {
verifyFileNameUriFromLaunch(fileName)
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
}
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class)
fun launchForAutofillResult(
activity: Activity,
fileName: String,
keyFile: String,
assistStructure: AssistStructure?) {
verifyFileNameUriFromLaunch(fileName)
if (assistStructure != null) {
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
assistStructure)
}
} else {
launch(activity, fileName, keyFile)
}
}
}
}

View File

@@ -17,7 +17,7 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.dialogs
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
@@ -34,30 +34,29 @@ import android.widget.CompoundButton
import android.widget.TextView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.fileselect.KeyFileHelper
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
import com.kunzisoft.keepass.utils.EmptyUtils
import com.kunzisoft.keepass.utils.UriUtil
class AssignMasterKeyDialogFragment : DialogFragment() {
private var masterPassword: String? = null
private var mKeyfile: Uri? = null
private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null
private var rootView: View? = null
private var passwordCheckBox: CompoundButton? = null
private var passView: TextView? = null
private var passConfView: TextView? = null
private var keyfileCheckBox: CompoundButton? = null
private var keyfileView: TextView? = null
private var keyFileCheckBox: CompoundButton? = null
private var keyFileView: TextView? = null
private var mListener: AssignPasswordDialogListener? = null
private var keyFileHelper: KeyFileHelper? = null
private var mKeyFileHelper: KeyFileHelper? = null
interface AssignPasswordDialogListener {
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?)
fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?)
}
@@ -67,17 +66,16 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
try {
mListener = activity as AssignPasswordDialogListener?
} catch (e: ClassCastException) {
throw ClassCastException(activity!!.toString()
throw ClassCastException(activity?.toString()
+ " must implement " + AssignPasswordDialogListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { notNullActivity ->
val builder = AlertDialog.Builder(notNullActivity)
val inflater = notNullActivity.layoutInflater
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
rootView = inflater.inflate(R.layout.set_password, null)
builder.setView(rootView)
@@ -99,51 +97,49 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
})
passConfView = rootView?.findViewById(R.id.pass_conf_password)
keyfileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyfileView = rootView?.findViewById(R.id.pass_keyfile)
keyfileView?.addTextChangedListener(object : TextWatcher {
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
keyFileView?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
keyfileCheckBox?.isChecked = true
keyFileCheckBox?.isChecked = true
}
})
keyFileHelper = KeyFileHelper(this)
mKeyFileHelper = KeyFileHelper(this)
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
keyFileHelper?.openFileOnClickViewListener?.onClick(view) }
mKeyFileHelper?.openFileOnClickViewListener?.onClick(view) }
val dialog = builder.create()
if (passwordCheckBox != null && keyfileCheckBox!= null) {
if (passwordCheckBox != null && keyFileCheckBox!= null) {
dialog.setOnShowListener { dialog1 ->
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
masterPassword = ""
mKeyfile = null
mMasterPassword = ""
mKeyFile = null
var error = verifyPassword() || verifyFile()
if (!passwordCheckBox!!.isChecked && !keyfileCheckBox!!.isChecked) {
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
error = true
showNoKeyConfirmationDialog()
}
if (!error) {
mListener!!.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, masterPassword,
keyfileCheckBox!!.isChecked, mKeyfile)
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
dismiss()
}
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
mListener!!.onAssignKeyDialogNegativeClick(
passwordCheckBox!!.isChecked, masterPassword,
keyfileCheckBox!!.isChecked, mKeyfile)
mListener?.onAssignKeyDialogNegativeClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
dismiss()
}
}
@@ -157,18 +153,21 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private fun verifyPassword(): Boolean {
var error = false
if (passwordCheckBox!!.isChecked) {
masterPassword = passView!!.text.toString()
val confpass = passConfView!!.text.toString()
if (passwordCheckBox != null
&& passwordCheckBox!!.isChecked
&& passView != null
&& passConfView != null) {
mMasterPassword = passView!!.text.toString()
val confPassword = passConfView!!.text.toString()
// Verify that passwords match
if (masterPassword != confpass) {
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
Toast.makeText(context, R.string.error_pass_match, Toast.LENGTH_LONG).show()
}
if (masterPassword == null || masterPassword!!.isEmpty()) {
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
error = true
showEmptyPasswordConfirmationDialog()
}
@@ -178,12 +177,14 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private fun verifyFile(): Boolean {
var error = false
if (keyfileCheckBox!!.isChecked) {
val keyfile = UriUtil.parseDefaultFile(keyfileView!!.text.toString())
mKeyfile = keyfile
if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked
&& keyFileView != null) {
val keyFile = UriUtil.parseDefaultFile(keyFileView!!.text.toString())
mKeyFile = keyFile
// Verify that a keyfile is set
if (EmptyUtils.isNullOrEmpty(keyfile)) {
if (EmptyUtils.isNullOrEmpty(keyFile)) {
error = true
Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show()
}
@@ -192,43 +193,46 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
private fun showEmptyPasswordConfirmationDialog() {
val builder = AlertDialog.Builder(activity!!)
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyFile()) {
mListener!!.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, masterPassword,
keyfileCheckBox!!.isChecked, mKeyfile)
this@AssignMasterKeyDialogFragment.dismiss()
activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyFile()) {
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
this@AssignMasterKeyDialogFragment.dismiss()
}
}
}
.setNegativeButton(R.string.cancel) { _, _ -> }
builder.create().show()
.setNegativeButton(R.string.cancel) { _, _ -> }
builder.create().show()
}
}
private fun showNoKeyConfirmationDialog() {
val builder = AlertDialog.Builder(activity!!)
builder.setMessage(R.string.warning_no_encryption_key)
.setPositiveButton(android.R.string.ok) { _, _ ->
mListener!!.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, masterPassword,
keyfileCheckBox!!.isChecked, mKeyfile)
this@AssignMasterKeyDialogFragment.dismiss()
}
.setNegativeButton(R.string.cancel) { _, _ -> }
builder.create().show()
activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_no_encryption_key)
.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
this@AssignMasterKeyDialogFragment.dismiss()
}
.setNegativeButton(R.string.cancel) { _, _ -> }
builder.create().show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
keyFileHelper!!.onActivityResultCallback(requestCode, resultCode, data
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
val pathString = UriUtil.parseDefaultFile(uri.toString())
if (pathString != null) {
keyfileCheckBox!!.isChecked = true
keyfileView!!.text = pathString.toString()
uri?.let { currentUri ->
UriUtil.parseDefaultFile(currentUri.toString())?.let { pathString ->
keyFileCheckBox?.isChecked = true
keyFileView?.text = pathString.toString()
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.widget.Button
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
class BrowserDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
// Get the layout inflater
val root = activity.layoutInflater.inflate(R.layout.browser_install, null)
builder.setView(root)
.setNegativeButton(R.string.cancel) { _, _ -> }
val market = root.findViewById<Button>(R.id.install_market)
market.setOnClickListener {
Util.gotoUrl(context, R.string.filemanager_play_store)
dismiss()
}
val web = root.findViewById<Button>(R.id.install_web)
web.setOnClickListener {
Util.gotoUrl(context, R.string.filemanager_f_droid)
dismiss()
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
}

View File

@@ -0,0 +1,211 @@
/*
* 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.Activity
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.FilePickerStylishActivity
import com.kunzisoft.keepass.utils.UriUtil
import com.nononsenseapps.filepicker.FilePickerActivity
import com.nononsenseapps.filepicker.Utils
class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedListener {
private val FILE_CODE = 3853
private var folderPathView: EditText? = null
private var fileNameView: EditText? = null
private var positiveButton: Button? = null
private var negativeButton: Button? = null
private var mDefinePathDialogListener: DefinePathDialogListener? = null
private var mDatabaseFileExtension: String? = null
private var mUriPath: Uri? = null
interface DefinePathDialogListener {
fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean
fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean
}
override fun onAttach(activity: Context?) {
super.onAttach(activity)
try {
mDefinePathDialogListener = activity as DefinePathDialogListener?
} catch (e: ClassCastException) {
throw ClassCastException(activity?.toString()
+ " must implement " + DefinePathDialogListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
val rootView = inflater.inflate(R.layout.file_creation, null)
builder.setView(rootView)
.setTitle(R.string.create_keepass_file)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(R.string.cancel) { _, _ -> }
// To prevent crash issue #69 https://github.com/Kunzisoft/KeePassDX/issues/69
val actionCopyBarCallback = object : ActionMode.Callback {
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
positiveButton?.isEnabled = false
negativeButton?.isEnabled = false
return true
}
override fun onDestroyActionMode(mode: ActionMode) {
positiveButton?.isEnabled = true
negativeButton?.isEnabled = true
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
return true
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return true
}
}
// Folder selection
val browseView = rootView.findViewById<View>(R.id.browse_button)
folderPathView = rootView.findViewById(R.id.folder_path)
folderPathView?.customSelectionActionModeCallback = actionCopyBarCallback
fileNameView = rootView.findViewById(R.id.filename)
fileNameView?.customSelectionActionModeCallback = actionCopyBarCallback
val defaultPath = Environment.getExternalStorageDirectory().path + getString(R.string.database_file_path_default)
folderPathView?.setText(defaultPath)
browseView.setOnClickListener { _ ->
Intent(context, FilePickerStylishActivity::class.java).apply {
putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
putExtra(FilePickerActivity.EXTRA_START_PATH,
Environment.getExternalStorageDirectory().path)
startActivityForResult(this, FILE_CODE)
}
}
// Init path
mUriPath = null
// Extension
mDatabaseFileExtension = getString(R.string.database_file_extension_default)
val spinner = rootView.findViewById<Spinner>(R.id.file_types)
spinner.onItemSelectedListener = this
// Spinner Drop down elements
val fileTypes = resources.getStringArray(R.array.file_types)
val dataAdapter = ArrayAdapter(activity!!, android.R.layout.simple_spinner_item, fileTypes)
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = dataAdapter
// Or text if only one item https://github.com/Kunzisoft/KeePassDX/issues/105
if (fileTypes.size == 1) {
val params = spinner.layoutParams
spinner.visibility = View.GONE
val extensionTextView = TextView(context)
extensionTextView.text = mDatabaseFileExtension
extensionTextView.layoutParams = params
val parentView = spinner.parent as ViewGroup
parentView.addView(extensionTextView)
}
val dialog = builder.create()
dialog.setOnShowListener { dialog1 ->
positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
positiveButton?.setOnClickListener { _ ->
mDefinePathDialogListener?.let {
if (it.onDefinePathDialogPositiveClick(buildPath()))
dismiss()
}
}
negativeButton?.setOnClickListener { _->
mDefinePathDialogListener?.let {
if (it.onDefinePathDialogNegativeClick(buildPath())) {
dismiss()
}
}
}
}
return dialog
}
return super.onCreateDialog(savedInstanceState)
}
private fun buildPath(): Uri? {
if (folderPathView != null && mDatabaseFileExtension != null) {
var path = Uri.Builder().path(folderPathView!!.text.toString())
.appendPath(fileNameView!!.text.toString() + mDatabaseFileExtension!!)
.build()
path = UriUtil.translate(context, path)
return path
}
return null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == FILE_CODE && resultCode == Activity.RESULT_OK) {
mUriPath = data?.data
mUriPath?.let {
val file = Utils.getFileForUri(it)
folderPathView?.setText(file.path)
}
}
}
override fun onItemSelected(adapterView: AdapterView<*>, view: View, position: Int, id: Long) {
mDatabaseFileExtension = adapterView.getItemAtPosition(position).toString()
}
override fun onNothingSelected(adapterView: AdapterView<*>) {
// Do nothing
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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.net.Uri
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.View
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
import java.text.DateFormat
class FileInformationDialogFragment : DialogFragment() {
private var fileSizeContainerView: View? = null
private var fileModificationContainerView: View? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
val root = inflater.inflate(R.layout.file_selection_information, null)
val fileNameView = root.findViewById<TextView>(R.id.file_filename)
val filePathView = root.findViewById<TextView>(R.id.file_path)
fileSizeContainerView = root.findViewById(R.id.file_size_container)
val fileSizeView = root.findViewById<TextView>(R.id.file_size)
fileModificationContainerView = root.findViewById(R.id.file_modification_container)
val fileModificationView = root.findViewById<TextView>(R.id.file_modification)
arguments?.apply {
if (containsKey(FILE_SELECT_BEEN_ARG)) {
(getSerializable(FILE_SELECT_BEEN_ARG) as FileDatabaseModel?)?.let { fileDatabaseModel ->
fileDatabaseModel.fileUri?.let { fileUri ->
filePathView.text = Uri.decode(fileUri.toString())
}
fileNameView.text = fileDatabaseModel.fileName
if (fileDatabaseModel.notFound()) {
hideFileInfo()
} else {
showFileInfo()
fileSizeView.text = fileDatabaseModel.size.toString()
fileModificationView.text = DateFormat.getDateTimeInstance()
.format(fileDatabaseModel.lastModification)
}
} ?: hideFileInfo()
}
}
builder.setView(root)
builder.setPositiveButton(android.R.string.ok) { _, _ -> }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun showFileInfo() {
fileSizeContainerView?.visibility = View.VISIBLE
fileModificationContainerView?.visibility = View.VISIBLE
}
private fun hideFileInfo() {
fileSizeContainerView?.visibility = View.GONE
fileModificationContainerView?.visibility = View.GONE
}
companion object {
private const val FILE_SELECT_BEEN_ARG = "FILE_SELECT_BEEN_ARG"
fun newInstance(fileDatabaseModel: FileDatabaseModel): FileInformationDialogFragment {
val fileInformationDialogFragment = FileInformationDialogFragment()
val args = Bundle()
args.putSerializable(FILE_SELECT_BEEN_ARG, fileDatabaseModel)
fileInformationDialogFragment.arguments = args
return fileInformationDialogFragment
}
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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.content.Context
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.View
import android.widget.*
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.Util
class GeneratePasswordDialogFragment : DialogFragment() {
private var mListener: GeneratePasswordListener? = null
private var root: View? = null
private var lengthTextView: EditText? = null
private var passwordView: EditText? = null
private var uppercaseBox: CompoundButton? = null
private var lowercaseBox: CompoundButton? = null
private var digitsBox: CompoundButton? = null
private var minusBox: CompoundButton? = null
private var underlineBox: CompoundButton? = null
private var spaceBox: CompoundButton? = null
private var specialsBox: CompoundButton? = null
private var bracketsBox: CompoundButton? = null
private var extendedBox: CompoundButton? = null
override fun onAttach(context: Context?) {
super.onAttach(context)
try {
mListener = context as GeneratePasswordListener?
} catch (e: ClassCastException) {
throw ClassCastException(context?.toString()
+ " must implement " + GeneratePasswordListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
root = inflater.inflate(R.layout.generate_password, null)
passwordView = root?.findViewById(R.id.password)
Util.applyFontVisibilityTo(context, passwordView)
lengthTextView = root?.findViewById(R.id.length)
uppercaseBox = root?.findViewById(R.id.cb_uppercase)
lowercaseBox = root?.findViewById(R.id.cb_lowercase)
digitsBox = root?.findViewById(R.id.cb_digits)
minusBox = root?.findViewById(R.id.cb_minus)
underlineBox = root?.findViewById(R.id.cb_underline)
spaceBox = root?.findViewById(R.id.cb_space)
specialsBox = root?.findViewById(R.id.cb_specials)
bracketsBox = root?.findViewById(R.id.cb_brackets)
extendedBox = root?.findViewById(R.id.cb_extended)
assignDefaultCharacters()
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
lengthTextView?.setText(progress.toString())
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
root?.findViewById<Button>(R.id.generate_password_button)
?.setOnClickListener { fillPassword() }
builder.setView(root)
.setPositiveButton(R.string.accept) { _, _ ->
val bundle = Bundle()
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString())
mListener?.acceptPassword(bundle)
dismiss()
}
.setNegativeButton(R.string.cancel) { _, _ ->
val bundle = Bundle()
mListener?.cancelPassword(bundle)
dismiss()
}
// Pre-populate a password to possibly save the user a few clicks
fillPassword()
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun assignDefaultCharacters() {
uppercaseBox?.isChecked = false
lowercaseBox?.isChecked = false
digitsBox?.isChecked = false
minusBox?.isChecked = false
underlineBox?.isChecked = false
spaceBox?.isChecked = false
specialsBox?.isChecked = false
bracketsBox?.isChecked = false
extendedBox?.isChecked = false
val defaultPasswordChars = PreferencesUtil.getDefaultPasswordCharacters(context)
for (passwordChar in defaultPasswordChars) {
when (passwordChar) {
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
}
}
}
private fun fillPassword() {
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
}
fun generatePassword(): String {
var password = ""
try {
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
val generator = PasswordGenerator(activity)
password = generator.generatePassword(length,
uppercaseBox?.isChecked == true,
lowercaseBox?.isChecked == true,
digitsBox?.isChecked == true,
minusBox?.isChecked == true,
underlineBox?.isChecked == true,
spaceBox?.isChecked == true,
specialsBox?.isChecked == true,
bracketsBox?.isChecked == true,
extendedBox?.isChecked == true)
} catch (e: NumberFormatException) {
Toast.makeText(context, R.string.error_wrong_length, Toast.LENGTH_LONG).show()
} catch (e: IllegalArgumentException) {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
return password
}
interface GeneratePasswordListener {
fun acceptPassword(bundle: Bundle)
fun cancelPassword(bundle: Bundle)
}
companion object {
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
}
}

View File

@@ -0,0 +1,199 @@
/*
* 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.content.Context
import android.graphics.Color
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.GroupVersioned
import com.kunzisoft.keepass.database.element.PwIcon
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
private var mDatabase: Database? = null
private var editGroupListener: EditGroupListener? = null
private var editGroupDialogAction: EditGroupDialogAction? = null
private var nameGroup: String? = null
private var iconGroup: PwIcon? = null
private var iconButton: ImageView? = null
private var iconColor: Int = 0
enum class EditGroupDialogAction {
CREATION, UPDATE, NONE;
companion object {
fun getActionFromOrdinal(ordinal: Int): EditGroupDialogAction {
return values()[ordinal]
}
}
}
override fun onAttach(context: Context?) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
// Instantiate the NoticeDialogListener so we can send events to the host
editGroupListener = context as EditGroupListener?
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context?.toString()
+ " must implement " + GroupEditDialogFragment::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.group_edit, null)
val nameField = root?.findViewById<TextView>(R.id.group_edit_name)
iconButton = root?.findViewById(R.id.group_edit_icon_button)
// Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
iconColor = ta.getColor(0, Color.WHITE)
ta.recycle()
// Init elements
mDatabase = App.currentDatabase
editGroupDialogAction = EditGroupDialogAction.NONE
nameGroup = ""
iconGroup = mDatabase?.iconFactory?.folderIcon
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_ACTION_ID)
&& savedInstanceState.containsKey(KEY_NAME)
&& savedInstanceState.containsKey(KEY_ICON)) {
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
nameGroup = savedInstanceState.getString(KEY_NAME)
iconGroup = savedInstanceState.getParcelable(KEY_ICON)
} else {
arguments?.apply {
if (containsKey(KEY_ACTION_ID))
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) {
nameGroup = getString(KEY_NAME)
iconGroup = getParcelable(KEY_ICON)
}
}
}
// populate the name
nameField?.text = nameGroup
// populate the icon
assignIconView()
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
editGroupListener?.approveEditGroup(
editGroupDialogAction,
nameField?.text.toString(),
iconGroup)
this@GroupEditDialogFragment.dialog.cancel()
}
.setNegativeButton(R.string.cancel) { _, _ ->
editGroupListener?.cancelEditGroup(
editGroupDialogAction,
nameField?.text.toString(),
iconGroup)
this@GroupEditDialogFragment.dialog.cancel()
}
iconButton?.setOnClickListener { _ ->
fragmentManager?.let {
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
}
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun assignIconView() {
mDatabase?.drawFactory
?.assignDatabaseIconTo(
context,
iconButton,
iconGroup,
iconColor)
}
override fun iconPicked(bundle: Bundle) {
iconGroup = bundle.getParcelable(IconPickerDialogFragment.KEY_ICON_STANDARD)
assignIconView()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal)
outState.putString(KEY_NAME, nameGroup)
outState.putParcelable(KEY_ICON, iconGroup)
super.onSaveInstanceState(outState)
}
interface EditGroupListener {
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
}
companion object {
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
const val KEY_NAME = "KEY_NAME"
const val KEY_ICON = "KEY_ICON"
const val KEY_ACTION_ID = "KEY_ACTION_ID"
fun build(): GroupEditDialogFragment {
val bundle = Bundle()
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
val fragment = GroupEditDialogFragment()
fragment.arguments = bundle
return fragment
}
fun build(group: GroupVersioned): GroupEditDialogFragment {
val bundle = Bundle()
bundle.putString(KEY_NAME, group.title)
bundle.putParcelable(KEY_ICON, group.icon)
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
val fragment = GroupEditDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -0,0 +1,138 @@
/*
* 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.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.widget.ImageViewCompat
import android.support.v7.app.AlertDialog
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.GridView
import android.widget.ImageView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.element.PwIconStandard
import com.kunzisoft.keepass.icons.IconPack
import com.kunzisoft.keepass.icons.IconPackChooser
class IconPickerDialogFragment : DialogFragment() {
private var iconPickerListener: IconPickerListener? = null
private var iconPack: IconPack? = null
override fun onAttach(context: Context?) {
super.onAttach(context)
try {
iconPickerListener = context as IconPickerListener?
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context!!.toString()
+ " must implement " + IconPickerListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
iconPack = IconPackChooser.getSelectedIconPack(context)
// Inflate and set the layout for the dialog
// Pass null as the parent view because its going in the dialog layout
val root = activity.layoutInflater.inflate(R.layout.icon_picker, null)
builder.setView(root)
val currIconGridView = root.findViewById<GridView>(R.id.IconGridView)
currIconGridView.adapter = ImageAdapter(activity)
currIconGridView.setOnItemClickListener { _, _, position, _ ->
val bundle = Bundle()
bundle.putParcelable(KEY_ICON_STANDARD, PwIconStandard(position))
iconPickerListener?.iconPicked(bundle)
dismiss()
}
builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog.cancel() }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() {
override fun getCount(): Int {
return iconPack?.numberOfIcons() ?: 0
}
override fun getItem(position: Int): Any? {
return null
}
override fun getItemId(position: Int): Long {
return 0
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val currentView: View = convertView
?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.inflate(R.layout.icon, parent, false)
iconPack?.let { iconPack ->
val iconImageView = currentView.findViewById<ImageView>(R.id.icon_image)
iconImageView.setImageResource(iconPack.iconToResId(position))
// Assign color if icons are tintable
if (iconPack.tintable()) {
// Retrieve the textColor to tint the icon
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
ta?.recycle()
}
}
return currentView
}
}
interface IconPickerListener {
fun iconPicked(bundle: Bundle)
}
companion object {
const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
fun launch(activity: StylishActivity) {
// Create an instance of the dialog fragment and show it
val dialog = IconPickerDialogFragment()
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.os.Bundle
import android.provider.Settings
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.View
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
class KeyboardExplanationDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let {
val builder = AlertDialog.Builder(activity!!)
val inflater = activity!!.layoutInflater
val rootView = inflater.inflate(R.layout.keyboard_explanation, null)
rootView.findViewById<View>(R.id.keyboards_activate_setting_path1_text)
.setOnClickListener { launchActivateKeyboardSetting() }
rootView.findViewById<View>(R.id.keyboards_activate_setting_path2_text)
.setOnClickListener { launchActivateKeyboardSetting() }
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
if (BuildConfig.CLOSED_STORE) {
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context, R.string.keyboard_switcher_play_store) }
} else {
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context, R.string.keyboard_switcher_f_droid) }
}
builder.setView(rootView)
.setPositiveButton(android.R.string.ok) { _, _ -> }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun launchActivateKeyboardSetting() {
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
}

View File

@@ -17,7 +17,7 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.dialogs
package com.kunzisoft.keepass.activities.dialogs
import android.app.AlertDialog
import android.content.Context

View File

@@ -0,0 +1,75 @@
/*
* 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.content.ActivityNotFoundException
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.text.Html
import android.text.SpannableStringBuilder
import android.widget.Toast
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
/**
* Custom Dialog that asks the user to download the pro version or make a donation.
*/
class ProFeatureDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder()
if (BuildConfig.CLOSED_STORE) {
// TODO HtmlCompat with androidX
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_ad_free))).append("\n\n")
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_buy_pro)))
builder.setPositiveButton(R.string.download) { _, _ ->
try {
Util.gotoUrl(context, R.string.app_pro_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
}
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_feature_generosity))).append("\n\n")
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_donation)))
builder.setPositiveButton(R.string.contribute) { _, _ ->
try {
Util.gotoUrl(context, R.string.contribution_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
}
}
builder.setMessage(stringBuilder)
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.AlertDialog
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import com.kunzisoft.keepass.R
class ReadOnlyDialog(context: Context) : AlertDialog(context) {
override fun onCreate(savedInstanceState: Bundle) {
val ctx = context
var warning = ctx.getString(R.string.read_only_warning)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
warning = warning + "\n\n" + context.getString(R.string.read_only_kitkat_warning)
}
setMessage(warning)
setButton(BUTTON_POSITIVE, ctx.getText(android.R.string.ok)) { _, _ -> dismiss() }
setButton(BUTTON_NEGATIVE, ctx.getText(R.string.beta_dontask)) { _, _ ->
val prefs = PreferenceManager.getDefaultSharedPreferences(ctx)
val edit = prefs.edit()
edit.putBoolean(ctx.getString(R.string.show_read_only_warning), false)
edit.apply()
dismiss()
}
super.onCreate(savedInstanceState)
}
}

View File

@@ -0,0 +1,189 @@
/*
* 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.content.Context
import android.os.Bundle
import android.support.annotation.IdRes
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.View
import android.widget.CompoundButton
import android.widget.RadioGroup
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.SortNodeEnum
class SortDialogFragment : DialogFragment() {
private var mListener: SortSelectionListener? = null
private var mSortNodeEnum: SortNodeEnum? = null
@IdRes
private var mCheckedId: Int = 0
private var mGroupsBefore: Boolean = false
private var mAscending: Boolean = false
private var mRecycleBinBottom: Boolean = false
override fun onAttach(context: Context?) {
super.onAttach(context)
try {
mListener = context as SortSelectionListener?
} catch (e: ClassCastException) {
throw ClassCastException(context!!.toString()
+ " must implement " + SortSelectionListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
mSortNodeEnum = SortNodeEnum.TITLE
mAscending = true
mGroupsBefore = true
var recycleBinAllowed = false
mRecycleBinBottom = true
arguments?.apply {
if (containsKey(SORT_NODE_ENUM_BUNDLE_KEY))
getString(SORT_NODE_ENUM_BUNDLE_KEY)?.let {
mSortNodeEnum = SortNodeEnum.valueOf(it)
}
if (containsKey(SORT_ASCENDING_BUNDLE_KEY))
mAscending = getBoolean(SORT_ASCENDING_BUNDLE_KEY)
if (containsKey(SORT_GROUPS_BEFORE_BUNDLE_KEY))
mGroupsBefore = getBoolean(SORT_GROUPS_BEFORE_BUNDLE_KEY)
if (containsKey(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY)) {
recycleBinAllowed = true
mRecycleBinBottom = getBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY)
}
}
mCheckedId = retrieveViewFromEnum(mSortNodeEnum!!)
val rootView = activity.layoutInflater.inflate(R.layout.sort_selection, null)
builder.setTitle(R.string.sort_menu)
builder.setView(rootView)
// Add action buttons
.setPositiveButton(android.R.string.ok
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum!!, mAscending, mGroupsBefore, mRecycleBinBottom) }
.setNegativeButton(R.string.cancel) { _, _ -> }
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
// Check if is ascending or descending
ascendingView.isChecked = mAscending
ascendingView.setOnCheckedChangeListener { _, isChecked -> mAscending = isChecked }
val groupsBeforeView = rootView.findViewById<CompoundButton>(R.id.sort_selection_groups_before)
// Check if groups before
groupsBeforeView.isChecked = mGroupsBefore
groupsBeforeView.setOnCheckedChangeListener { _, isChecked -> mGroupsBefore = isChecked }
val recycleBinBottomView = rootView.findViewById<CompoundButton>(R.id.sort_selection_recycle_bin_bottom)
if (!recycleBinAllowed) {
recycleBinBottomView.visibility = View.GONE
} else {
// Check if recycle bin at the bottom
recycleBinBottomView.isChecked = mRecycleBinBottom
recycleBinBottomView.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked }
}
val sortSelectionRadioGroupView = rootView.findViewById<RadioGroup>(R.id.sort_selection_radio_group)
// Check value by default
sortSelectionRadioGroupView.check(mCheckedId)
sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId -> mSortNodeEnum = retrieveSortEnumFromViewId(checkedId) }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
@IdRes
private fun retrieveViewFromEnum(sortNodeEnum: SortNodeEnum): Int {
return when (sortNodeEnum) {
SortNodeEnum.DB -> R.id.sort_selection_db
SortNodeEnum.TITLE -> R.id.sort_selection_title
SortNodeEnum.USERNAME -> R.id.sort_selection_username
SortNodeEnum.CREATION_TIME -> R.id.sort_selection_creation_time
SortNodeEnum.LAST_MODIFY_TIME -> R.id.sort_selection_last_modify_time
SortNodeEnum.LAST_ACCESS_TIME -> R.id.sort_selection_last_access_time
}
}
private fun retrieveSortEnumFromViewId(@IdRes checkedId: Int): SortNodeEnum {
// Change enum
return when (checkedId) {
R.id.sort_selection_db -> SortNodeEnum.DB
R.id.sort_selection_title -> SortNodeEnum.TITLE
R.id.sort_selection_username -> SortNodeEnum.USERNAME
R.id.sort_selection_creation_time -> SortNodeEnum.CREATION_TIME
R.id.sort_selection_last_modify_time -> SortNodeEnum.LAST_MODIFY_TIME
R.id.sort_selection_last_access_time -> SortNodeEnum.LAST_ACCESS_TIME
else -> SortNodeEnum.TITLE
}
}
interface SortSelectionListener {
fun onSortSelected(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean)
}
companion object {
private const val SORT_NODE_ENUM_BUNDLE_KEY = "SORT_NODE_ENUM_BUNDLE_KEY"
private const val SORT_ASCENDING_BUNDLE_KEY = "SORT_ASCENDING_BUNDLE_KEY"
private const val SORT_GROUPS_BEFORE_BUNDLE_KEY = "SORT_GROUPS_BEFORE_BUNDLE_KEY"
private const val SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY = "SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY"
private fun buildBundle(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean): Bundle {
val bundle = Bundle()
bundle.putString(SORT_NODE_ENUM_BUNDLE_KEY, sortNodeEnum.name)
bundle.putBoolean(SORT_ASCENDING_BUNDLE_KEY, ascending)
bundle.putBoolean(SORT_GROUPS_BEFORE_BUNDLE_KEY, groupsBefore)
return bundle
}
fun getInstance(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean): SortDialogFragment {
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
val fragment = SortDialogFragment()
fragment.arguments = bundle
return fragment
}
fun getInstance(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean): SortDialogFragment {
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
val fragment = SortDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.widget.TextView
import com.kunzisoft.keepass.R
class UnavailableFeatureDialogFragment : DialogFragment() {
private var minVersionRequired = Build.VERSION_CODES.M
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
arguments?.apply {
if (containsKey(MIN_REQUIRED_VERSION_ARG))
minVersionRequired = getInt(MIN_REQUIRED_VERSION_ARG)
}
val rootView = activity.layoutInflater.inflate(R.layout.unavailable_feature, null)
val messageView = rootView.findViewById<TextView>(R.id.unavailable_feature_message)
val builder = AlertDialog.Builder(activity)
val message = SpannableStringBuilder()
message.append(getString(R.string.unavailable_feature_text))
.append("\n\n")
if (Build.VERSION.SDK_INT < minVersionRequired) {
message.append(getString(R.string.unavailable_feature_version,
androidNameFromApiNumber(Build.VERSION.SDK_INT, Build.VERSION.RELEASE),
androidNameFromApiNumber(minVersionRequired)))
message.append("\n\n")
.append(Html.fromHtml("<a href=\"https://source.android.com/setup/build-numbers\">CodeNames</a>"))
} else
message.append(getString(R.string.unavailable_feature_hardware))
messageView.text = message
messageView.movementMethod = LinkMovementMethod.getInstance()
builder.setView(rootView)
.setPositiveButton(android.R.string.ok) { _, _ -> }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun androidNameFromApiNumber(apiNumber: Int, releaseVersion: String = ""): String {
var version = releaseVersion
val builder = StringBuilder()
val fields = Build.VERSION_CODES::class.java.fields
var apiName = ""
for (field in fields) {
val fieldName = field.name
var fieldValue = -1
try {
fieldValue = field.getInt(Any())
} catch (e: IllegalArgumentException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
} catch (e: NullPointerException) {
e.printStackTrace()
}
if (fieldValue == apiNumber) {
apiName = fieldName
}
}
if (apiName.isEmpty()) {
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO")
val index = apiNumber - 1
apiName = if (index < mapper.size) mapper[index] else "UNKNOWN_VERSION"
}
if (version.isEmpty()) {
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0")
val index = apiNumber - 1
version = if (index < versions.size) versions[index] else "UNKNOWN_VERSION"
}
builder.append("\n\t")
if (apiName.isNotEmpty())
builder.append(apiName).append(" ")
if (version.isNotEmpty())
builder.append(version).append(" ")
builder.append("(API ").append(apiNumber).append(")")
builder.append("\n")
return builder.toString()
}
companion object {
private const val MIN_REQUIRED_VERSION_ARG = "MIN_REQUIRED_VERSION_ARG"
fun getInstance(minVersionRequired: Int): UnavailableFeatureDialogFragment {
val fragment = UnavailableFeatureDialogFragment()
val args = Bundle()
args.putInt(MIN_REQUIRED_VERSION_ARG, minVersionRequired)
fragment.arguments = args
return fragment
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.content.ActivityNotFoundException
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.text.Html
import android.text.SpannableStringBuilder
import android.widget.Toast
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
/**
* Custom Dialog that asks the user to download the pro version or make a donation.
*/
class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity!!)
val stringBuilder = SpannableStringBuilder()
if (BuildConfig.CLOSED_STORE) {
if (BuildConfig.FULL_VERSION) {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature_thanks))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_rose))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_work_hard))).append("\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_upgrade))).append(" ")
builder.setPositiveButton(android.R.string.ok) { dialog, id -> dismiss() }
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_buy_pro))).append("\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
builder.setPositiveButton(R.string.download) { dialog, id ->
try {
Util.gotoUrl(context, R.string.app_pro_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
}
builder.setNegativeButton(android.R.string.cancel) { dialog, id -> dismiss() }
}
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_contibute))).append(" ")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
builder.setPositiveButton(R.string.contribute) { dialog, id ->
try {
Util.gotoUrl(context, R.string.contribution_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
}
builder.setNegativeButton(android.R.string.cancel) { dialog, id -> dismiss() }
}
builder.setMessage(stringBuilder)
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
}

View File

@@ -17,7 +17,7 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.compat;
package com.kunzisoft.keepass.activities.helpers;
import android.content.Intent;
import android.net.Uri;

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.activities
package com.kunzisoft.keepass.activities.helpers
import android.app.assist.AssistStructure
import android.content.Intent

View File

@@ -0,0 +1,213 @@
/*
* 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.helpers
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
import android.util.Log
import android.view.View
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
import com.kunzisoft.keepass.fileselect.StorageAF
import com.kunzisoft.keepass.utils.Interaction
import com.kunzisoft.keepass.utils.UriUtil
class KeyFileHelper {
private var activity: Activity? = null
private var fragment: Fragment? = null
val openFileOnClickViewListener: OpenFileOnClickViewListener
get() = OpenFileOnClickViewListener(null)
constructor(context: Activity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri)?) : View.OnClickListener {
override fun onClick(v: View) {
try {
if (StorageAF.useStorageFramework(activity)) {
openActivityWithActionOpenDocument()
} else {
openActivityWithActionGetContent()
}
} catch (e: Exception) {
Log.e(TAG, "Enable to start the file picker activity", e)
// Open File picker if can't open activity
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
showBrowserDialog()
}
}
}
private fun openActivityWithActionOpenDocument() {
val i = Intent(StorageAF.ACTION_OPEN_DOCUMENT)
i.addCategory(Intent.CATEGORY_OPENABLE)
i.type = "*/*"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
} else {
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
}
if (fragment != null)
fragment?.startActivityForResult(i, OPEN_DOC)
else
activity?.startActivityForResult(i, OPEN_DOC)
}
private fun openActivityWithActionGetContent() {
val i = Intent(Intent.ACTION_GET_CONTENT)
i.addCategory(Intent.CATEGORY_OPENABLE)
i.type = "*/*"
if (fragment != null)
fragment?.startActivityForResult(i, GET_CONTENT)
else
activity?.startActivityForResult(i, GET_CONTENT)
}
fun getOpenFileOnClickViewListener(dataUri: () -> Uri): OpenFileOnClickViewListener {
return OpenFileOnClickViewListener(dataUri)
}
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
var showBrowser = false
try {
if (Interaction.isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
// Get file path parent if possible
if (dataUri != null
&& dataUri.toString().isNotEmpty()
&& dataUri.scheme == "file") {
intent.data = dataUri
} else {
Log.w(javaClass.name, "Unable to read the URI")
}
if (fragment != null)
fragment?.startActivityForResult(intent, FILE_BROWSE)
else
activity?.startActivityForResult(intent, FILE_BROWSE)
} else {
showBrowser = true
}
} catch (e: Exception) {
Log.w(TAG, "Enable to start OPEN_INTENTS_FILE_BROWSE", e)
showBrowser = true
}
return showBrowser
}
/**
* Show Browser dialog to select file picker app
*/
private fun showBrowserDialog() {
try {
val browserDialogFragment = BrowserDialogFragment()
if (fragment != null && fragment!!.fragmentManager != null)
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
else if (activity!!.fragmentManager != null)
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
} catch (e: Exception) {
Log.e(TAG, "Can't open BrowserDialog", e)
}
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param keyFileCallback Callback retrieve from data
* @return true if requestCode was captured, false elsechere
*/
fun onActivityResultCallback(
requestCode: Int,
resultCode: Int,
data: Intent?,
keyFileCallback: ((uri: Uri?) -> Unit)?): Boolean {
when (requestCode) {
FILE_BROWSE -> {
if (resultCode == RESULT_OK) {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parseDefaultFile(filename)
}
keyFileCallback?.invoke(keyUri)
}
return true
}
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
var uri = data.data
if (uri != null) {
if (StorageAF.useStorageFramework(activity)) {
try {
// try to persist read and write permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity?.contentResolver?.apply {
takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_READ_URI_PERMISSION)
takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
} catch (e: Exception) {
// nop
}
}
if (requestCode == GET_CONTENT) {
uri = UriUtil.translate(activity, uri)
}
keyFileCallback?.invoke(uri)
}
}
}
return true
}
}
return false
}
companion object {
private const val TAG = "KeyFileHelper"
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
private const val GET_CONTENT = 25745
private const val OPEN_DOC = 25845
private const val FILE_BROWSE = 25645
}
}

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.activities
package com.kunzisoft.keepass.activities.helpers
import android.content.Context
import android.content.Intent

View File

@@ -0,0 +1,88 @@
package com.kunzisoft.keepass.activities.helpers
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
import com.kunzisoft.keepass.utils.UriUtil
import java.io.File
import java.lang.ref.WeakReference
class UriIntentInitTask(private val weakContext: WeakReference<Context>,
private val uriIntentInitTaskCallback: UriIntentInitTaskCallback,
private val isKeyFileNeeded: Boolean)
: AsyncTask<Intent, Void, Int>() {
private var databaseUri: Uri? = null
private var keyFileUri: Uri? = null
override fun doInBackground(vararg args: Intent): Int? {
val intent = args[0]
val action = intent.action
// If is a view intent
if (action != null && action == VIEW_INTENT) {
val incoming = intent.data
databaseUri = incoming
keyFileUri = ClipDataCompat.getUriFromIntent(intent, KEY_KEYFILE)
if (incoming == null) {
return R.string.error_can_not_handle_uri
} else if (incoming.scheme == "file") {
val fileName = incoming.path
if (fileName?.isNotEmpty() == true) {
// No file name
return R.string.file_not_found
}
val dbFile = File(fileName)
if (!dbFile.exists()) {
// File does not exist
return R.string.file_not_found
}
if (keyFileUri == null) {
keyFileUri = getKeyFileUri(databaseUri)
}
} else if (incoming.scheme == "content") {
if (keyFileUri == null) {
keyFileUri = getKeyFileUri(databaseUri)
}
} else {
return R.string.error_can_not_handle_uri
}
} else {
databaseUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_FILENAME))
keyFileUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_KEYFILE))
if (keyFileUri == null || keyFileUri!!.toString().isEmpty()) {
keyFileUri = getKeyFileUri(databaseUri)
}
}
return null
}
public override fun onPostExecute(result: Int?) {
uriIntentInitTaskCallback.onPostInitTask(databaseUri, keyFileUri, result)
}
private fun getKeyFileUri(databaseUri: Uri?): Uri? {
return if (isKeyFileNeeded) {
FileDatabaseHistory.getInstance(weakContext).getKeyFileUriByDatabaseUri(databaseUri!!)
} else {
null
}
}
companion object {
const val KEY_FILENAME = "fileName"
const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018 Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
@@ -17,10 +17,10 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.password;
package com.kunzisoft.keepass.activities.helpers
import android.net.Uri;
import android.net.Uri
interface UriIntentInitTaskCallback {
void onPostInitTask(Uri dbUri, Uri keyFileUri, Integer errorStringId);
fun onPostInitTask(dbUri: Uri?, keyFileUri: Uri?, errorStringId: Int?)
}

View File

@@ -28,11 +28,11 @@ import android.content.IntentFilter
import android.os.Bundle
import android.util.Log
import android.view.View
import com.kunzisoft.keepass.activities.EntrySelectionHelper
import com.kunzisoft.keepass.activities.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.stylish.StylishActivity
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.timeout.TimeoutHelper
abstract class LockingActivity : StylishActivity() {
@@ -88,7 +88,7 @@ abstract class LockingActivity : StylishActivity() {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_EXIT_LOCK) {
exitLock = true
if (App.getDB().loaded) {
if (App.currentDatabase.loaded) {
lockAndExit()
}
}
@@ -102,7 +102,7 @@ abstract class LockingActivity : StylishActivity() {
if (timeoutEnable) {
// End activity if database not loaded
if (!App.getDB().loaded) {
if (!App.currentDatabase.loaded) {
finish()
return
}
@@ -163,9 +163,9 @@ abstract class LockingActivity : StylishActivity() {
/**
* To reset the app timeout when a view is focused or changed
*/
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View) {
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
views.forEach {
it.setOnFocusChangeListener { _, hasFocus ->
it?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
}
@@ -190,7 +190,7 @@ fun Activity.lock() {
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
cancelAll()
}
App.getDB().closeAndClear(applicationContext)
App.currentDatabase.closeAndClear(applicationContext)
setResult(LockingActivity.RESULT_EXIT_LOCK)
finish()
}

View File

@@ -17,7 +17,7 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.fileselect;
package com.kunzisoft.keepass.activities.stylish;
import android.content.Context;
import android.os.Bundle;
@@ -26,7 +26,6 @@ import android.support.annotation.StyleRes;
import android.util.Log;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.stylish.Stylish;
import com.nononsenseapps.filepicker.FilePickerActivity;
/**

View File

@@ -17,7 +17,7 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.stylish;
package com.kunzisoft.keepass.activities.stylish;
import android.content.Context;
import android.support.annotation.StyleRes;

View File

@@ -17,7 +17,7 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.stylish;
package com.kunzisoft.keepass.activities.stylish;
import android.os.Bundle;
import android.support.annotation.Nullable;

View File

@@ -17,7 +17,7 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.stylish;
package com.kunzisoft.keepass.activities.stylish;
import android.content.Context;
import android.content.res.TypedArray;

View File

@@ -0,0 +1,150 @@
/*
* 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.adapters
import android.content.Context
import android.net.Uri
import android.support.annotation.ColorInt
import android.support.v7.widget.RecyclerView
import android.util.TypedValue
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
import com.kunzisoft.keepass.settings.PreferencesUtil
class FileDatabaseHistoryAdapter(private val context: Context, private val listFiles: List<String>)
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var fileItemOpenListener: FileItemOpenListener? = null
private var fileSelectClearListener: FileSelectClearListener? = null
private var fileInformationShowListener: FileInformationShowListener? = null
@ColorInt
private val defaultColor: Int
@ColorInt
private val warningColor: Int
init {
val typedValue = TypedValue()
val theme = context.theme
theme.resolveAttribute(R.attr.colorAccentCompat, typedValue, true)
warningColor = typedValue.data
theme.resolveAttribute(android.R.attr.textColorHintInverse, typedValue, true)
defaultColor = typedValue.data
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
val view = inflater.inflate(R.layout.file_row, parent, false)
return FileDatabaseHistoryViewHolder(view)
}
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
val fileDatabaseModel = FileDatabaseModel(context, listFiles[position])
// Context menu creation
holder.fileContainer.setOnCreateContextMenuListener(ContextMenuBuilder(fileDatabaseModel))
// Click item to open file
if (fileItemOpenListener != null)
holder.fileContainer.setOnClickListener(FileItemClickListener(position))
// Assign file name
if (PreferencesUtil.isFullFilePathEnable(context))
holder.fileName.text = Uri.decode(fileDatabaseModel.fileUri.toString())
else
holder.fileName.text = fileDatabaseModel.fileName
holder.fileName.textSize = PreferencesUtil.getListTextSize(context)
// Click on information
if (fileInformationShowListener != null)
holder.fileInformation.setOnClickListener(FileInformationClickListener(fileDatabaseModel))
}
override fun getItemCount(): Int {
return listFiles.size
}
fun setOnItemClickListener(fileItemOpenListener: FileItemOpenListener) {
this.fileItemOpenListener = fileItemOpenListener
}
fun setFileSelectClearListener(fileSelectClearListener: FileSelectClearListener) {
this.fileSelectClearListener = fileSelectClearListener
}
fun setFileInformationShowListener(fileInformationShowListener: FileInformationShowListener) {
this.fileInformationShowListener = fileInformationShowListener
}
interface FileItemOpenListener {
fun onFileItemOpenListener(itemPosition: Int)
}
interface FileSelectClearListener {
fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean
}
interface FileInformationShowListener {
fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel)
}
private inner class FileItemClickListener(private val position: Int) : View.OnClickListener {
override fun onClick(v: View) {
fileItemOpenListener?.onFileItemOpenListener(position)
}
}
private inner class FileInformationClickListener(private val fileDatabaseModel: FileDatabaseModel) : View.OnClickListener {
override fun onClick(view: View) {
fileInformationShowListener?.onClickFileInformation(fileDatabaseModel)
}
}
private inner class ContextMenuBuilder(private val fileDatabaseModel: FileDatabaseModel) : View.OnCreateContextMenuListener {
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
if (fileSelectClearListener == null)
return@OnMenuItemClickListener false
when (item.itemId) {
MENU_CLEAR -> fileSelectClearListener!!.onFileSelectClearListener(fileDatabaseModel)
else -> false
}
}
override fun onCreateContextMenu(contextMenu: ContextMenu?, view: View?, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
contextMenu?.add(Menu.NONE, MENU_CLEAR, Menu.NONE, R.string.remove_from_filelist)
?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
}
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var fileContainer: View = itemView.findViewById(R.id.file_container)
var fileName: TextView = itemView.findViewById(R.id.file_filename)
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
}
companion object {
private const val MENU_CLEAR = 1
}
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2018 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.adapters;
import android.view.View;
import com.kunzisoft.keepass.R;
class GroupViewHolder extends BasicViewHolder {
GroupViewHolder(View itemView) {
super(itemView);
container = itemView.findViewById(R.id.group_container);
icon = itemView.findViewById(R.id.group_icon);
text = itemView.findViewById(R.id.group_text);
subText = itemView.findViewById(R.id.group_subtext);
}
}

View File

@@ -1,406 +0,0 @@
/*
* Copyright 2018 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.adapters;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.support.annotation.NonNull;
import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.util.SortedListAdapterCallback;
import android.util.Log;
import android.view.*;
import android.widget.Toast;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.app.App;
import com.kunzisoft.keepass.database.SortNodeEnum;
import com.kunzisoft.keepass.database.element.*;
import com.kunzisoft.keepass.settings.PreferencesUtil;
import com.kunzisoft.keepass.utils.Util;
public class NodeAdapter extends RecyclerView.Adapter<BasicViewHolder> {
private static final String TAG = NodeAdapter.class.getName();
private SortedList<NodeVersioned> nodeSortedList;
private Context context;
private LayoutInflater inflater;
private MenuInflater menuInflater;
private float textSize;
private float subtextSize;
private float iconSize;
private SortNodeEnum listSort;
private boolean groupsBeforeSort;
private boolean ascendingSort;
private boolean showUsernames;
private NodeClickCallback nodeClickCallback;
private NodeMenuListener nodeMenuListener;
private boolean activateContextMenu;
private boolean readOnly;
private boolean isASearchResult;
private Database database;
private int iconGroupColor;
private int iconEntryColor;
/**
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
public NodeAdapter(final Context context, MenuInflater menuInflater) {
this.inflater = LayoutInflater.from(context);
this.menuInflater = menuInflater;
this.context = context;
assignPreferences();
this.activateContextMenu = false;
this.readOnly = false;
this.isASearchResult = false;
this.nodeSortedList = new SortedList<>(NodeVersioned.class, new SortedListAdapterCallback<NodeVersioned>(this) {
@Override public int compare(NodeVersioned item1, NodeVersioned item2) {
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort).compare(item1, item2);
}
@Override public boolean areContentsTheSame(NodeVersioned oldItem, NodeVersioned newItem) {
return oldItem.getTitle().equals(newItem.getTitle())
&& oldItem.getIcon().equals(newItem.getIcon());
}
@Override public boolean areItemsTheSame(NodeVersioned item1, NodeVersioned item2) {
return item1.equals(item2);
}
});
// Database
this.database = App.getDB();
// Retrieve the color to tint the icon
int[] attrTextColorPrimary = {android.R.attr.textColorPrimary};
TypedArray taTextColorPrimary = context.getTheme().obtainStyledAttributes(attrTextColorPrimary);
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK);
taTextColorPrimary.recycle();
int[] attrTextColor = {android.R.attr.textColor}; // In two times to fix bug compilation
TypedArray taTextColor = context.getTheme().obtainStyledAttributes(attrTextColor);
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK);
taTextColor.recycle();
}
public void setReadOnly(boolean readOnly) {
this.readOnly = readOnly;
}
public void setIsASearchResult(boolean isASearchResult) {
this.isASearchResult = isASearchResult;
}
public void setActivateContextMenu(boolean activate) {
this.activateContextMenu = activate;
}
private void assignPreferences() {
float textSizeDefault = Util.getListTextDefaultSize(context);
this.textSize = PreferencesUtil.getListTextSize(context);
this.subtextSize = context.getResources().getInteger(R.integer.list_small_size_default)
* textSize / textSizeDefault;
// Retrieve the icon size
float iconDefaultSize = context.getResources().getDimension(R.dimen.list_icon_size_default);
this.iconSize = iconDefaultSize * textSize / textSizeDefault;
this.listSort = PreferencesUtil.getListSort(context);
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context);
this.ascendingSort = PreferencesUtil.getAscendingSort(context);
this.showUsernames = PreferencesUtil.showUsernamesListEntries(context);
}
/**
* Rebuild the list by clear and build children from the group
*/
public void rebuildList(GroupVersioned group) {
this.nodeSortedList.clear();
assignPreferences();
// TODO verify sort
try {
this.nodeSortedList.addAll(group.getChildrenWithoutMetaStream());
} catch (Exception e) {
Log.e(TAG, "Can't add node elements to the list", e);
Toast.makeText(context, "Can't add node elements to the list : " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
/**
* Determine if the adapter contains or not any element
* @return true if the list is empty
*/
public boolean isEmpty() {
return nodeSortedList.size() <= 0;
}
/**
* Add a node to the list
* @param node Node to add
*/
public void addNode(NodeVersioned node) {
nodeSortedList.add(node);
}
/**
* Remove a node in the list
* @param node Node to delete
*/
public void removeNode(NodeVersioned node) {
nodeSortedList.remove(node);
}
/**
* Update a node in the list
* @param oldNode Node before the update
* @param newNode Node after the update
*/
public void updateNode(NodeVersioned oldNode, NodeVersioned newNode) {
nodeSortedList.beginBatchedUpdates();
nodeSortedList.remove(oldNode);
nodeSortedList.add(newNode);
nodeSortedList.endBatchedUpdates();
}
/**
* Notify a change sort of the list
*/
public void notifyChangeSort(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore) {
this.listSort = sortNodeEnum;
this.ascendingSort = ascending;
this.groupsBeforeSort = groupsBefore;
}
@Override
public int getItemViewType(int position) {
return nodeSortedList.get(position).getType().ordinal();
}
@NonNull
@Override
public BasicViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
BasicViewHolder basicViewHolder;
View view;
if (viewType == Type.GROUP.ordinal()) {
view = inflater.inflate(R.layout.list_nodes_group, parent, false);
basicViewHolder = new GroupViewHolder(view);
} else {
view = inflater.inflate(R.layout.list_nodes_entry, parent, false);
basicViewHolder = new EntryViewHolder(view);
}
return basicViewHolder;
}
@Override
public void onBindViewHolder(@NonNull BasicViewHolder holder, int position) {
NodeVersioned subNode = nodeSortedList.get(position);
// Assign image
int iconColor = Color.BLACK;
switch (subNode.getType()) {
case GROUP:
iconColor = iconGroupColor;
break;
case ENTRY:
iconColor = iconEntryColor;
break;
}
database.getDrawFactory().assignDatabaseIconTo(context, holder.icon, subNode.getIcon(), iconColor);
// Assign text
holder.text.setText(subNode.getTitle());
// Assign click
holder.container.setOnClickListener(
new OnNodeClickListener(subNode));
// Context menu
if (activateContextMenu) {
holder.container.setOnCreateContextMenuListener(
new ContextMenuBuilder(subNode, nodeMenuListener, readOnly));
}
// Add username
holder.subText.setText("");
holder.subText.setVisibility(View.GONE);
if (subNode.getType().equals(Type.ENTRY)) {
EntryVersioned entry = (EntryVersioned) subNode;
database.startManageEntry(entry);
holder.text.setText(entry.getVisualTitle());
String username = entry.getUsername();
if (showUsernames && !username.isEmpty()) {
holder.subText.setVisibility(View.VISIBLE);
holder.subText.setText(username);
}
database.stopManageEntry(entry);
}
// Assign image and text size
// Relative size of the icon
holder.icon.getLayoutParams().height = ((int) iconSize);
holder.icon.getLayoutParams().width = ((int) iconSize);
holder.text.setTextSize(textSize);
holder.subText.setTextSize(subtextSize);
}
@Override
public int getItemCount() {
return nodeSortedList.size();
}
/**
* Assign a listener when a node is clicked
*/
public void setOnNodeClickListener(NodeClickCallback nodeClickCallback) {
this.nodeClickCallback = nodeClickCallback;
}
/**
* Assign a listener when an element of menu is clicked
*/
public void setNodeMenuListener(NodeMenuListener nodeMenuListener) {
this.nodeMenuListener = nodeMenuListener;
}
/**
* Callback listener to redefine to do an action when a node is click
*/
public interface NodeClickCallback {
void onNodeClick(NodeVersioned node);
}
/**
* Menu listener to redefine to do an action in menu
*/
public interface NodeMenuListener {
boolean onOpenMenuClick(NodeVersioned node);
boolean onEditMenuClick(NodeVersioned node);
boolean onCopyMenuClick(NodeVersioned node);
boolean onMoveMenuClick(NodeVersioned node);
boolean onDeleteMenuClick(NodeVersioned node);
}
/**
* Utility class for node listener
*/
private class OnNodeClickListener implements View.OnClickListener {
private NodeVersioned node;
OnNodeClickListener(NodeVersioned node) {
this.node = node;
}
@Override
public void onClick(View v) {
if (nodeClickCallback != null)
nodeClickCallback.onNodeClick(node);
}
}
/**
* Utility class for menu listener
*/
private class ContextMenuBuilder implements View.OnCreateContextMenuListener {
private NodeVersioned node;
private NodeMenuListener menuListener;
private boolean readOnly;
ContextMenuBuilder(NodeVersioned node, NodeMenuListener menuListener, boolean readOnly) {
this.menuListener = menuListener;
this.node = node;
this.readOnly = readOnly;
}
@Override
public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) {
menuInflater.inflate(R.menu.node_menu, contextMenu);
// Opening
MenuItem menuItem = contextMenu.findItem(R.id.menu_open);
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
Database database = App.getDB();
// Edition
if (readOnly || node.equals(database.getRecycleBin())) {
contextMenu.removeItem(R.id.menu_edit);
} else {
menuItem = contextMenu.findItem(R.id.menu_edit);
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
}
// Copy (not for group)
if (readOnly
|| isASearchResult
|| node.equals(database.getRecycleBin())
|| node.getType().equals(Type.GROUP)) {
// TODO COPY For Group
contextMenu.removeItem(R.id.menu_copy);
} else {
menuItem = contextMenu.findItem(R.id.menu_copy);
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
}
// Move
if (readOnly
|| isASearchResult
|| node.equals(database.getRecycleBin())) {
contextMenu.removeItem(R.id.menu_move);
} else {
menuItem = contextMenu.findItem(R.id.menu_move);
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
}
// Deletion
if (readOnly || node.equals(database.getRecycleBin())) {
contextMenu.removeItem(R.id.menu_delete);
} else {
menuItem = contextMenu.findItem(R.id.menu_delete);
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
}
}
private MenuItem.OnMenuItemClickListener mOnMyActionClickListener = new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
if (menuListener == null)
return false;
switch ( item.getItemId() ) {
case R.id.menu_open:
return menuListener.onOpenMenuClick(node);
case R.id.menu_edit:
return menuListener.onEditMenuClick(node);
case R.id.menu_copy:
return menuListener.onCopyMenuClick(node);
case R.id.menu_move:
return menuListener.onMoveMenuClick(node);
case R.id.menu_delete:
return menuListener.onDeleteMenuClick(node);
default:
return false;
}
}
};
}
}

View File

@@ -0,0 +1,390 @@
/*
* 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.adapters
import android.content.Context
import android.graphics.Color
import android.support.v7.util.SortedList
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.util.SortedListAdapterCallback
import android.util.Log
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.Util
class NodeAdapter
/**
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
(private val context: Context, private val menuInflater: MenuInflater)
: RecyclerView.Adapter<NodeAdapter.BasicViewHolder>() {
private val nodeSortedList: SortedList<NodeVersioned>
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var textSize: Float = 0.toFloat()
private var subtextSize: Float = 0.toFloat()
private var iconSize: Float = 0.toFloat()
private var listSort: SortNodeEnum? = null
private var groupsBeforeSort: Boolean = false
private var ascendingSort: Boolean = false
private var showUserNames: Boolean = false
private var nodeClickCallback: NodeClickCallback? = null
private var nodeMenuListener: NodeMenuListener? = null
private var activateContextMenu: Boolean = false
private var readOnly: Boolean = false
private var isASearchResult: Boolean = false
private val mDatabase: Database
private val iconGroupColor: Int
private val iconEntryColor: Int
/**
* Determine if the adapter contains or not any element
* @return true if the list is empty
*/
val isEmpty: Boolean
get() = nodeSortedList.size() <= 0
init {
assignPreferences()
this.activateContextMenu = false
this.readOnly = false
this.isASearchResult = false
this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback<NodeVersioned>(this) {
override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int {
return listSort?.getNodeComparator(ascendingSort, groupsBeforeSort)?.compare(item1, item2) ?: 0
}
override fun areContentsTheSame(oldItem: NodeVersioned, newItem: NodeVersioned): Boolean {
return oldItem.title == newItem.title && oldItem.icon == newItem.icon
}
override fun areItemsTheSame(item1: NodeVersioned, item2: NodeVersioned): Boolean {
return item1 == item2
}
})
// Database
this.mDatabase = App.currentDatabase
// Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
taTextColorPrimary.recycle()
// In two times to fix bug compilation
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK)
taTextColor.recycle()
}
fun setReadOnly(readOnly: Boolean) {
this.readOnly = readOnly
}
fun setIsASearchResult(isASearchResult: Boolean) {
this.isASearchResult = isASearchResult
}
fun setActivateContextMenu(activate: Boolean) {
this.activateContextMenu = activate
}
private fun assignPreferences() {
val textSizeDefault = Util.getListTextDefaultSize(context)
this.textSize = PreferencesUtil.getListTextSize(context)
this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault
// Retrieve the icon size
val iconDefaultSize = context.resources.getDimension(R.dimen.list_icon_size_default)
this.iconSize = iconDefaultSize * textSize / textSizeDefault
this.listSort = PreferencesUtil.getListSort(context)
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
}
/**
* Rebuild the list by clear and build children from the group
*/
fun rebuildList(group: GroupVersioned) {
this.nodeSortedList.clear()
assignPreferences()
// TODO verify sort
try {
this.nodeSortedList.addAll(group.getChildrenWithoutMetaStream())
} 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()
}
}
/**
* Add a node to the list
* @param node Node to add
*/
fun addNode(node: NodeVersioned) {
nodeSortedList.add(node)
}
/**
* Remove a node in the list
* @param node Node to delete
*/
fun removeNode(node: NodeVersioned) {
nodeSortedList.remove(node)
}
/**
* Update a node in the list
* @param oldNode Node before the update
* @param newNode Node after the update
*/
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
nodeSortedList.beginBatchedUpdates()
nodeSortedList.remove(oldNode)
nodeSortedList.add(newNode)
nodeSortedList.endBatchedUpdates()
}
/**
* Notify a change sort of the list
*/
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) {
this.listSort = sortNodeEnum
this.ascendingSort = ascending
this.groupsBeforeSort = groupsBefore
}
override fun getItemViewType(position: Int): Int {
return nodeSortedList.get(position).type.ordinal
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicViewHolder {
val basicViewHolder: BasicViewHolder
val view: View
if (viewType == Type.GROUP.ordinal) {
view = inflater.inflate(R.layout.list_nodes_group, parent, false)
basicViewHolder = GroupViewHolder(view)
} else {
view = inflater.inflate(R.layout.list_nodes_entry, parent, false)
basicViewHolder = EntryViewHolder(view)
}
return basicViewHolder
}
override fun onBindViewHolder(holder: BasicViewHolder, position: Int) {
val subNode = nodeSortedList.get(position)
// Assign image
val iconColor = when (subNode.type) {
Type.GROUP -> iconGroupColor
Type.ENTRY -> iconEntryColor
}
mDatabase.drawFactory.assignDatabaseIconTo(context, holder.icon, subNode.icon, iconColor)
// Assign text
holder.text?.text = subNode.title
// Assign click
holder.container?.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) }
// Context menu
if (activateContextMenu) {
holder.container?.setOnCreateContextMenuListener(
ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener))
}
// Add username
holder.subText?.text = ""
holder.subText?.visibility = View.GONE
if (subNode.type == Type.ENTRY) {
val entry = subNode as EntryVersioned
mDatabase.startManageEntry(entry)
holder.text?.text = entry.getVisualTitle()
val username = entry.username
if (showUserNames && username.isNotEmpty()) {
holder.subText?.visibility = View.VISIBLE
holder.subText?.text = username
}
mDatabase.stopManageEntry(entry)
}
// Assign image and text size
// Relative size of the icon
holder.icon?.layoutParams?.height = iconSize.toInt()
holder.icon?.layoutParams?.width = iconSize.toInt()
holder.text?.textSize = textSize
holder.subText?.textSize = subtextSize
}
override fun getItemCount(): Int {
return nodeSortedList.size()
}
/**
* Assign a listener when a node is clicked
*/
fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) {
this.nodeClickCallback = nodeClickCallback
}
/**
* Assign a listener when an element of menu is clicked
*/
fun setNodeMenuListener(nodeMenuListener: NodeMenuListener?) {
this.nodeMenuListener = nodeMenuListener
}
/**
* Callback listener to redefine to do an action when a node is click
*/
interface NodeClickCallback {
fun onNodeClick(node: NodeVersioned)
}
/**
* Menu listener to redefine to do an action in menu
*/
interface NodeMenuListener {
fun onOpenMenuClick(node: NodeVersioned): Boolean
fun onEditMenuClick(node: NodeVersioned): Boolean
fun onCopyMenuClick(node: NodeVersioned): Boolean
fun onMoveMenuClick(node: NodeVersioned): Boolean
fun onDeleteMenuClick(node: NodeVersioned): Boolean
}
/**
* Utility class for menu listener
*/
private class ContextMenuBuilder(val menuInflater: MenuInflater,
val node: NodeVersioned,
val readOnly: Boolean,
val isASearchResult: Boolean,
val menuListener: NodeMenuListener?)
: View.OnCreateContextMenuListener {
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
if (menuListener == null)
return@OnMenuItemClickListener false
when (item.itemId) {
R.id.menu_open -> menuListener.onOpenMenuClick(node)
R.id.menu_edit -> menuListener.onEditMenuClick(node)
R.id.menu_copy -> menuListener.onCopyMenuClick(node)
R.id.menu_move -> menuListener.onMoveMenuClick(node)
R.id.menu_delete -> menuListener.onDeleteMenuClick(node)
else -> false
}
}
override fun onCreateContextMenu(contextMenu: ContextMenu?,
view: View?,
contextMenuInfo: ContextMenu.ContextMenuInfo?) {
menuInflater.inflate(R.menu.node_menu, contextMenu)
// Opening
var menuItem = contextMenu?.findItem(R.id.menu_open)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
val database = App.currentDatabase
// Edition
if (readOnly || node == database.recycleBin) {
contextMenu?.removeItem(R.id.menu_edit)
} else {
menuItem = contextMenu?.findItem(R.id.menu_edit)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
// Copy (not for group)
if (readOnly
|| isASearchResult
|| node == database.recycleBin
|| node.type == Type.GROUP) {
// TODO COPY For Group
contextMenu?.removeItem(R.id.menu_copy)
} else {
menuItem = contextMenu?.findItem(R.id.menu_copy)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
// Move
if (readOnly
|| isASearchResult
|| node == database.recycleBin) {
contextMenu?.removeItem(R.id.menu_move)
} else {
menuItem = contextMenu?.findItem(R.id.menu_move)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
// Deletion
if (readOnly || node == database.recycleBin) {
contextMenu?.removeItem(R.id.menu_delete)
} else {
menuItem = contextMenu?.findItem(R.id.menu_delete)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
}
}
abstract class BasicViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var container: View? = null
var icon: ImageView? = null
var text: TextView? = null
var subText: TextView? = null
}
internal class GroupViewHolder(itemView: View) : BasicViewHolder(itemView) {
init {
container = itemView.findViewById(R.id.group_container)
icon = itemView.findViewById(R.id.group_icon)
text = itemView.findViewById(R.id.group_text)
subText = itemView.findViewById(R.id.group_subtext)
}
}
internal class EntryViewHolder(itemView: View) : BasicViewHolder(itemView) {
init {
container = itemView.findViewById(R.id.entry_container)
icon = itemView.findViewById(R.id.entry_icon)
text = itemView.findViewById(R.id.entry_text)
subText = itemView.findViewById(R.id.entry_subtext)
}
}
companion object {
private val TAG = NodeAdapter::class.java.name
}
}

View File

@@ -1,138 +0,0 @@
/*
* Copyright 2018 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.adapters;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Color;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
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.EntryVersioned;
import com.kunzisoft.keepass.database.element.PwIcon;
import com.kunzisoft.keepass.database.element.PwIconFactory;
import com.kunzisoft.keepass.settings.PreferencesUtil;
import java.util.UUID;
public class SearchEntryCursorAdapter extends CursorAdapter {
private LayoutInflater cursorInflater;
private Database database;
private boolean displayUsername;
private int iconColor;
public SearchEntryCursorAdapter(Context context, Database database) {
super(context, null, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
cursorInflater = (LayoutInflater) context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
this.database = database;
// Get the icon color
int[] attrTextColor = {R.attr.textColorInverse};
TypedArray taTextColor = context.getTheme().obtainStyledAttributes(attrTextColor);
this.iconColor = taTextColor.getColor(0, Color.WHITE);
taTextColor.recycle();
reInit(context);
}
public void reInit(Context context) {
this.displayUsername = PreferencesUtil.showUsernamesListEntries(context);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = cursorInflater.inflate(R.layout.search_entry, parent ,false);
ViewHolder viewHolder = new ViewHolder();
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon);
viewHolder.textViewTitle = view.findViewById(R.id.entry_text);
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext);
view.setTag(viewHolder);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
// Retrieve elements from cursor
UUID uuid = new UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)));
PwIconFactory iconFactory = database.getIconFactory();
PwIcon icon = iconFactory.getIcon(
new 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.getKeyIcon();
}
String title = cursor.getString( cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_TITLE) );
String username = cursor.getString( cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_USERNAME) );
String url = cursor.getString( cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_URL) );
ViewHolder viewHolder = (ViewHolder) view.getTag();
// Assign image
database.getDrawFactory().assignDatabaseIconTo(context, viewHolder.imageViewIcon, icon, iconColor);
// Assign title
String showTitle = EntryVersioned.CREATOR.getVisualTitle(false, title, username, url, uuid.toString());
viewHolder.textViewTitle.setText(showTitle);
if (displayUsername && !username.isEmpty()) {
viewHolder.textViewSubTitle.setText(String.format("(%s)", username));
} else {
viewHolder.textViewSubTitle.setText("");
}
}
private static class ViewHolder {
ImageView imageViewIcon;
TextView textViewTitle;
TextView textViewSubTitle;
}
@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
return database.searchEntry(constraint.toString());
}
public EntryVersioned getEntryFromPosition(int position) {
EntryVersioned pwEntry = null;
Cursor cursor = this.getCursor();
if (cursor.moveToFirst()
&& cursor.move(position)) {
pwEntry = database.getEntryFrom(cursor);
}
return pwEntry;
}
}

View File

@@ -0,0 +1,124 @@
/*
* 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.adapters
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import android.support.v4.widget.CursorAdapter
import android.view.LayoutInflater
import android.view.View
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.EntryVersioned
import com.kunzisoft.keepass.database.element.PwIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
class SearchEntryCursorAdapter(context: Context, private val database: Database) : CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var displayUsername: Boolean = false
private val iconColor: Int
init {
// Get the icon color
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
this.iconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
reInit(context)
}
fun reInit(context: Context) {
this.displayUsername = PreferencesUtil.showUsernamesListEntries(context)
}
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
val view = cursorInflater.inflate(R.layout.search_entry, parent, false)
val viewHolder = ViewHolder()
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext)
view.tag = viewHolder
return view
}
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: PwIcon = 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))
val viewHolder = view.tag as ViewHolder
// Assign image
database.drawFactory.assignDatabaseIconTo(context, viewHolder.imageViewIcon, icon, iconColor)
// Assign title
val showTitle = EntryVersioned.getVisualTitle(false, title, username, url, uuid.toString())
viewHolder.textViewTitle?.text = showTitle
if (displayUsername && username.isNotEmpty()) {
viewHolder.textViewSubTitle?.text = String.format("(%s)", username)
} else {
viewHolder.textViewSubTitle?.text = ""
}
}
private class ViewHolder {
internal var imageViewIcon: ImageView? = null
internal var textViewTitle: TextView? = null
internal var textViewSubTitle: TextView? = null
}
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
return database.searchEntry(constraint.toString())
}
fun getEntryFromPosition(position: Int): EntryVersioned? {
var pwEntry: EntryVersioned? = null
val cursor = this.cursor
if (cursor.moveToFirst() && cursor.move(position)) {
pwEntry = database.getEntryFrom(cursor)
}
return pwEntry
}
}

View File

@@ -1,74 +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.app;
import android.support.multidex.MultiDexApplication;
import com.kunzisoft.keepass.compat.PRNGFixes;
import com.kunzisoft.keepass.database.element.Database;
import com.kunzisoft.keepass.fileselect.RecentFileHistory;
import com.kunzisoft.keepass.stylish.Stylish;
import java.util.Calendar;
public class App extends MultiDexApplication {
private static Database db = null;
private static Calendar calendar = null;
private static RecentFileHistory fileHistory = null;
public static Database getDB() {
if ( db == null ) {
db = new Database();
}
return db;
}
public static RecentFileHistory getFileHistory() {
return fileHistory;
}
public static void setDB(Database d) {
db = d;
}
public static Calendar getCalendar() {
if ( calendar == null ) {
calendar = Calendar.getInstance();
}
return calendar;
}
@Override
public void onCreate() {
super.onCreate();
Stylish.init(this);
fileHistory = new RecentFileHistory(this);
PRNGFixes.apply();
}
@Override
public void onTerminate() {
if ( db != null ) {
db.closeAndClear(getApplicationContext());
}
super.onTerminate();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018 Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
@@ -17,19 +17,27 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.adapters;
package com.kunzisoft.keepass.app
import android.view.View;
import android.support.multidex.MultiDexApplication
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.R;
class App : MultiDexApplication() {
class EntryViewHolder extends BasicViewHolder {
companion object {
var currentDatabase: Database = Database()
}
EntryViewHolder(View itemView) {
super(itemView);
container = itemView.findViewById(R.id.entry_container);
icon = itemView.findViewById(R.id.entry_icon);
text = itemView.findViewById(R.id.entry_text);
subText = itemView.findViewById(R.id.entry_subtext);
override fun onCreate() {
super.onCreate()
Stylish.init(this)
PRNGFixes.apply()
}
override fun onTerminate() {
currentDatabase.closeAndClear(applicationContext)
super.onTerminate()
}
}

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.compat;
package com.kunzisoft.keepass.app;
/*
* This software is provided 'as-is', without any express or implied

View File

@@ -28,9 +28,9 @@ import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.support.v7.app.AppCompatActivity
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.fileselect.FileSelectActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -41,10 +41,10 @@ class AutoFillLauncherActivity : AppCompatActivity() {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
if (assistStructure != null) {
if (App.getDB().loaded && TimeoutHelper.checkTime(this))
if (App.currentDatabase.loaded && TimeoutHelper.checkTime(this))
GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this))
else {
FileSelectActivity.launchForAutofillResult(this, assistStructure)
FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure)
}
} else {
setResult(Activity.RESULT_CANCELED)

View File

@@ -32,7 +32,7 @@ import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.database.element.EntryVersioned
import java.util.*
@@ -52,11 +52,11 @@ object AutofillHelper {
}
private fun makeEntryTitle(entry: EntryVersioned): String {
if (!entry.title.isEmpty() && !entry.username.isEmpty())
if (entry.title.isNotEmpty() && entry.username.isNotEmpty())
return String.format("%s (%s)", entry.title, entry.username)
if (!entry.title.isEmpty())
if (entry.title.isNotEmpty())
return entry.title
if (!entry.username.isEmpty())
if (entry.username.isNotEmpty())
return entry.username
return if (!entry.notes.isEmpty()) entry.notes.trim { it <= ' ' } else ""
// TODO No title
@@ -89,23 +89,26 @@ object AutofillHelper {
* Method to hit when right key is selected
*/
fun buildResponseWhenEntrySelected(activity: Activity, entry: EntryVersioned) {
val mReplyIntent: Intent
activity.intent?.let { intent ->
if (intent.extras.containsKey(ASSIST_STRUCTURE)) {
val structure = intent.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)
val result = StructureParser(structure).parse()
// New Response
val responseBuilder = FillResponse.Builder()
val dataset = buildDataset(activity, entry, result)
responseBuilder.addDataset(dataset)
mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
responseBuilder.build())
activity.setResult(Activity.RESULT_OK, mReplyIntent)
} else {
var setResultOk = false
activity.intent?.extras?.let { extras ->
if (extras.containsKey(ASSIST_STRUCTURE)) {
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
StructureParser(structure).parse()?.let { result ->
// New Response
val responseBuilder = FillResponse.Builder()
val dataset = buildDataset(activity, entry, result)
responseBuilder.addDataset(dataset)
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
responseBuilder.build())
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
}
}
}
if (!setResultOk) {
Log.w(activity.javaClass.name, "Failed Autofill auth.")
activity.setResult(Activity.RESULT_CANCELED)
}
@@ -118,7 +121,7 @@ object AutofillHelper {
fun startActivityForAutofillResult(activity: Activity, intent: Intent, assistStructure: AssistStructure) {
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
activity.startActivityForResult(intent, AutofillHelper.AUTOFILL_RESPONSE_REQUEST_CODE)
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
}
/**

View File

@@ -1,87 +0,0 @@
/*
* Copyright 2017 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.autofill;
import android.app.assist.AssistStructure;
import android.content.IntentSender;
import android.os.Build;
import android.os.CancellationSignal;
import android.service.autofill.AutofillService;
import android.service.autofill.FillCallback;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
import android.service.autofill.SaveCallback;
import android.service.autofill.SaveRequest;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.widget.RemoteViews;
import com.kunzisoft.keepass.R;
import java.util.Arrays;
import java.util.List;
@RequiresApi(api = Build.VERSION_CODES.O)
public class KeeAutofillService extends AutofillService {
private static final String TAG = "KeeAutofillService";
@Override
public void onFillRequest(@NonNull FillRequest request, @NonNull CancellationSignal cancellationSignal,
@NonNull FillCallback callback) {
List<FillContext> fillContexts = request.getFillContexts();
AssistStructure latestStructure = fillContexts.get(fillContexts.size() - 1).getStructure();
cancellationSignal.setOnCancelListener(() ->
Log.e(TAG, "Cancel autofill not implemented in this sample.")
);
FillResponse.Builder responseBuilder = new FillResponse.Builder();
// Check user's settings for authenticating Responses and Datasets.
StructureParser.Result parseResult = new StructureParser(latestStructure).parse();
AutofillId[] autofillIds = parseResult.allAutofillIds();
if (!Arrays.asList(autofillIds).isEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
IntentSender sender = AutoFillLauncherActivity.Companion.getAuthIntentSenderForResponse(this);
RemoteViews presentation = new RemoteViews(getPackageName(), R.layout.autofill_service_unlock);
responseBuilder.setAuthentication(autofillIds, sender, presentation);
callback.onSuccess(responseBuilder.build());
}
}
@Override
public void onSaveRequest(@NonNull SaveRequest request, @NonNull SaveCallback callback) {
// TODO Save autofill
//callback.onFailure(getString(R.string.autofill_not_support_save));
}
@Override
public void onConnected() {
Log.d(TAG, "onConnected");
}
@Override
public void onDisconnected() {
Log.d(TAG, "onDisconnected");
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.autofill
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.*
import android.support.annotation.RequiresApi
import android.util.Log
import android.widget.RemoteViews
import com.kunzisoft.keepass.R
@RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() {
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal,
callback: FillCallback) {
val fillContexts = request.fillContexts
val latestStructure = fillContexts[fillContexts.size - 1].structure
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") }
val responseBuilder = FillResponse.Builder()
// Check user's settings for authenticating Responses and Datasets.
val parseResult = StructureParser(latestStructure).parse()
parseResult?.allAutofillIds()?.let { autofillIds ->
if (listOf(*autofillIds).isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
val sender = AutoFillLauncherActivity.getAuthIntentSenderForResponse(this)
val presentation = RemoteViews(packageName, R.layout.autofill_service_unlock)
responseBuilder.setAuthentication(autofillIds, sender, presentation)
callback.onSuccess(responseBuilder.build())
}
}
}
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
// TODO Save autofill
//callback.onFailure(getString(R.string.autofill_not_support_save));
}
override fun onConnected() {
Log.d(TAG, "onConnected")
}
override fun onDisconnected() {
Log.d(TAG, "onDisconnected")
}
companion object {
private val TAG = KeeAutofillService::class.java.name
}
}

View File

@@ -1,115 +0,0 @@
/*
* Copyright 2018 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.autofill;
import android.app.assist.AssistStructure;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.text.InputType;
import android.util.Log;
import android.view.View;
import android.view.autofill.AutofillId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Parse AssistStructure and guess username and password fields.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
class StructureParser {
static private final String TAG = StructureParser.class.getName();
final private AssistStructure structure;
private Result result;
private AutofillId usernameCandidate;
StructureParser(AssistStructure structure) {
this.structure = structure;
}
Result parse() {
result = new Result();
usernameCandidate = null;
for (int i=0; i<structure.getWindowNodeCount(); ++i) {
AssistStructure.WindowNode windowNode = structure.getWindowNodeAt(i);
result.title.add(windowNode.getTitle());
result.webDomain.add(windowNode.getRootViewNode().getWebDomain());
parseViewNode(windowNode.getRootViewNode());
}
// If not explicit username field found, add the field just before password field.
if (result.username.isEmpty() && result.email.isEmpty()
&& !result.password.isEmpty() && usernameCandidate != null)
result.username.add(usernameCandidate);
return result;
}
private void parseViewNode(AssistStructure.ViewNode node) {
String[] hints = node.getAutofillHints();
if (hints != null && hints.length > 0) {
if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_USERNAME::equals))
result.username.add(node.getAutofillId());
else if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_EMAIL_ADDRESS::equals))
result.email.add(node.getAutofillId());
else if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_PASSWORD::equals))
result.password.add(node.getAutofillId());
else
Log.d(TAG, "unsupported hints");
} else if (node.getAutofillType() == View.AUTOFILL_TYPE_TEXT) {
int inputType = node.getInputType();
if ((inputType & InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) > 0)
result.email.add(node.getAutofillId());
else if ((inputType & InputType.TYPE_TEXT_VARIATION_PASSWORD) > 0)
result.password.add(node.getAutofillId());
else if (result.password.isEmpty())
usernameCandidate = node.getAutofillId();
}
for (int i=0; i<node.getChildCount(); ++i)
parseViewNode(node.getChildAt(i));
}
@RequiresApi(api = Build.VERSION_CODES.O)
static class Result {
final List<CharSequence> title;
final List<String> webDomain;
final List<AutofillId> username;
final List<AutofillId> email;
final List<AutofillId> password;
private Result() {
title = new ArrayList<>();
webDomain = new ArrayList<>();
username = new ArrayList<>();
email = new ArrayList<>();
password = new ArrayList<>();
}
AutofillId[] allAutofillIds() {
ArrayList<AutofillId> all = new ArrayList<>();
all.addAll(username);
all.addAll(email);
all.addAll(password);
return all.toArray(new AutofillId[0]);
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* 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.autofill
import android.app.assist.AssistStructure
import android.os.Build
import android.support.annotation.RequiresApi
import android.text.InputType
import android.util.Log
import android.view.View
import android.view.autofill.AutofillId
import java.util.*
/**
* Parse AssistStructure and guess username and password fields.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
internal class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null
private var usernameCandidate: AutofillId? = null
fun parse(): Result? {
result = Result()
result?.apply {
usernameCandidate = null
for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i)
title.add(windowNode.title)
windowNode.rootViewNode.webDomain?.let {
webDomain.add(it)
}
parseViewNode(windowNode.rootViewNode)
}
// If not explicit username field found, add the field just before password field.
if (username.isEmpty() && email.isEmpty()
&& password.isNotEmpty() && usernameCandidate != null)
username.add(usernameCandidate!!)
}
return result
}
private fun parseViewNode(node: AssistStructure.ViewNode) {
val hints = node.autofillHints
val autofillId = node.autofillId
if (autofillId != null) {
if (hints != null && hints.isNotEmpty()) {
when {
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId)
else -> Log.d(TAG, "unsupported hints")
}
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) {
val inputType = node.inputType
when {
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId)
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId)
result?.password?.isEmpty() == true -> usernameCandidate = autofillId
}
}
}
for (i in 0 until node.childCount)
parseViewNode(node.getChildAt(i))
}
@RequiresApi(api = Build.VERSION_CODES.O)
internal class Result {
val title: MutableList<CharSequence>
val webDomain: MutableList<String>
val username: MutableList<AutofillId>
val email: MutableList<AutofillId>
val password: MutableList<AutofillId>
init {
title = ArrayList()
webDomain = ArrayList()
username = ArrayList()
email = ArrayList()
password = ArrayList()
}
fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>()
all.addAll(username)
all.addAll(email)
all.addAll(password)
return all.toTypedArray()
}
}
companion object {
private val TAG = StructureParser::class.java.name
}
}

View File

@@ -1,39 +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.backup;
import android.annotation.SuppressLint;
import android.app.backup.BackupAgentHelper;
import android.app.backup.SharedPreferencesBackupHelper;
@SuppressLint("NewApi")
public class SettingsBackupAgent extends BackupAgentHelper {
private static final String PREFS_BACKUP_KEY = "prefs";
@Override
public void onCreate() {
String defaultPrefs = this.getPackageName() + "_preferences";
SharedPreferencesBackupHelper prefHelper = new SharedPreferencesBackupHelper(this, defaultPrefs);
addHelper(PREFS_BACKUP_KEY, prefHelper);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018 Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
@@ -17,21 +17,22 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.adapters;
package com.kunzisoft.keepass.backup
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.annotation.SuppressLint
import android.app.backup.BackupAgentHelper
import android.app.backup.SharedPreferencesBackupHelper
abstract class BasicViewHolder extends RecyclerView.ViewHolder {
@SuppressLint("NewApi")
class SettingsBackupAgent : BackupAgentHelper() {
View container;
ImageView icon;
TextView text;
TextView subText;
override fun onCreate() {
val defaultPrefs = this.packageName + "_preferences"
val prefHelper = SharedPreferencesBackupHelper(this, defaultPrefs)
addHelper(PREFS_BACKUP_KEY, prefHelper)
}
BasicViewHolder(View itemView) {
super(itemView);
companion object {
private const val PREFS_BACKUP_KEY = "prefs"
}
}

View File

@@ -23,14 +23,14 @@ import java.security.Provider;
public final class AESProvider extends Provider {
/**
*
*/
private static final long serialVersionUID = -3846349284296062658L;
/**
*
*/
private static final long serialVersionUID = -3846349284296062658L;
public AESProvider() {
super("AESProvider", 1.0, "");
put("Cipher.AES",NativeAESCipherSpi.class.getName());
}
public AESProvider() {
super("AESProvider", 1.0, "");
put("Cipher.AES",NativeAESCipherSpi.class.getName());
}
}

View File

@@ -38,58 +38,58 @@ import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
public class CipherFactory {
private static boolean blacklistInit = false;
private static boolean blacklisted;
private static boolean blacklistInit = false;
private static boolean blacklisted;
static {
Security.addProvider(new BouncyCastleProvider());
}
static {
Security.addProvider(new BouncyCastleProvider());
}
public static Cipher getInstance(String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException {
return getInstance(transformation, false);
}
public static Cipher getInstance(String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException {
return getInstance(transformation, false);
}
public static Cipher getInstance(String transformation, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException {
// Return the native AES if it is possible
if ( (!deviceBlacklisted()) && (!androidOverride) && hasNativeImplementation(transformation) && NativeLib.loaded() ) {
return Cipher.getInstance(transformation, new AESProvider());
} else {
public static Cipher getInstance(String transformation, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException {
// Return the native AES if it is possible
if ( (!deviceBlacklisted()) && (!androidOverride) && hasNativeImplementation(transformation) && NativeLib.loaded() ) {
return Cipher.getInstance(transformation, new AESProvider());
} else {
return Cipher.getInstance(transformation);
}
}
}
}
public static boolean deviceBlacklisted() {
if (!blacklistInit) {
blacklistInit = true;
public static boolean deviceBlacklisted() {
if (!blacklistInit) {
blacklistInit = true;
// The Acer Iconia A500 is special and seems to always crash in the native crypto libraries
blacklisted = Build.MODEL.equals("A500");
}
return blacklisted;
}
// The Acer Iconia A500 is special and seems to always crash in the native crypto libraries
blacklisted = Build.MODEL.equals("A500");
}
return blacklisted;
}
private static boolean hasNativeImplementation(String transformation) {
return transformation.equals("AES/CBC/PKCS5Padding");
}
private static boolean hasNativeImplementation(String transformation) {
return transformation.equals("AES/CBC/PKCS5Padding");
}
/** Generate appropriate cipher based on KeePass 2.x UUID's
* @param uuid
* @return
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws InvalidAlgorithmParameterException
* @throws InvalidKeyException
*/
public static CipherEngine getInstance(UUID uuid) throws NoSuchAlgorithmException {
if ( uuid.equals(AesEngine.CIPHER_UUID) ) {
return new AesEngine();
} else if ( uuid.equals(TwofishEngine.CIPHER_UUID) ) {
return new TwofishEngine();
} else if ( uuid.equals(ChaCha20Engine.CIPHER_UUID)) {
return new ChaCha20Engine();
}
/** Generate appropriate cipher based on KeePass 2.x UUID's
* @param uuid
* @return
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws InvalidAlgorithmParameterException
* @throws InvalidKeyException
*/
public static CipherEngine getInstance(UUID uuid) throws NoSuchAlgorithmException {
if ( uuid.equals(AesEngine.CIPHER_UUID) ) {
return new AesEngine();
} else if ( uuid.equals(TwofishEngine.CIPHER_UUID) ) {
return new TwofishEngine();
} else if ( uuid.equals(ChaCha20Engine.CIPHER_UUID)) {
return new ChaCha20Engine();
}
throw new NoSuchAlgorithmException("UUID unrecognized.");
}
throw new NoSuchAlgorithmException("UUID unrecognized.");
}
}

View File

@@ -44,284 +44,284 @@ import javax.crypto.spec.IvParameterSpec;
public class NativeAESCipherSpi extends CipherSpi {
private static final String TAG = NativeAESCipherSpi.class.getName();
private static boolean mIsStaticInit = false;
private static HashMap<PhantomReference<NativeAESCipherSpi>, Long> mCleanup = new HashMap<PhantomReference<NativeAESCipherSpi>, Long>();
private static ReferenceQueue<NativeAESCipherSpi> mQueue = new ReferenceQueue<NativeAESCipherSpi>();
private final int AES_BLOCK_SIZE = 16;
private byte[] mIV;
private boolean mIsInited = false;
private boolean mEncrypting = false;
private long mCtxPtr;
private boolean mPadding = false;
private static final String TAG = NativeAESCipherSpi.class.getName();
private static boolean mIsStaticInit = false;
private static HashMap<PhantomReference<NativeAESCipherSpi>, Long> mCleanup = new HashMap<PhantomReference<NativeAESCipherSpi>, Long>();
private static ReferenceQueue<NativeAESCipherSpi> mQueue = new ReferenceQueue<NativeAESCipherSpi>();
private final int AES_BLOCK_SIZE = 16;
private byte[] mIV;
private boolean mIsInited = false;
private boolean mEncrypting = false;
private long mCtxPtr;
private boolean mPadding = false;
private static void staticInit() {
mIsStaticInit = true;
private static void staticInit() {
mIsStaticInit = true;
// Start the cipher context cleanup thread to run forever
(new Thread(new Cleanup())).start();
}
// Start the cipher context cleanup thread to run forever
(new Thread(new Cleanup())).start();
}
private static void addToCleanupQueue(NativeAESCipherSpi ref, long ptr) {
Log.d(TAG, "queued cipher context: " + ptr);
mCleanup.put(new PhantomReference<NativeAESCipherSpi>(ref, mQueue), ptr);
}
private static void addToCleanupQueue(NativeAESCipherSpi ref, long ptr) {
Log.d(TAG, "queued cipher context: " + ptr);
mCleanup.put(new PhantomReference<NativeAESCipherSpi>(ref, mQueue), ptr);
}
/** Work with the garbage collector to clean up openssl memory when the cipher
* context is garbage collected.
* @author bpellin
*
*/
private static class Cleanup implements Runnable {
public void run() {
while (true) {
try {
Reference<? extends NativeAESCipherSpi> ref = mQueue.remove();
long ctx = mCleanup.remove(ref);
nCleanup(ctx);
Log.d(TAG, "Cleaned up cipher context: " + ctx);
} catch (InterruptedException e) {
// Do nothing, but resume looping if mQueue.remove is interrupted
}
}
}
}
private static native void nCleanup(long ctxPtr);
public NativeAESCipherSpi() {
if ( ! mIsStaticInit ) {
staticInit();
}
}
@Override
protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen)
throws IllegalBlockSizeException, BadPaddingException {
int maxSize = engineGetOutputSize(inputLen);
byte[] output = new byte[maxSize];
int finalSize;
/** Work with the garbage collector to clean up openssl memory when the cipher
* context is garbage collected.
* @author bpellin
*
*/
private static class Cleanup implements Runnable {
public void run() {
while (true) {
try {
Reference<? extends NativeAESCipherSpi> ref = mQueue.remove();
long ctx = mCleanup.remove(ref);
nCleanup(ctx);
Log.d(TAG, "Cleaned up cipher context: " + ctx);
} catch (InterruptedException e) {
// Do nothing, but resume looping if mQueue.remove is interrupted
}
}
}
}
private static native void nCleanup(long ctxPtr);
public NativeAESCipherSpi() {
if ( ! mIsStaticInit ) {
staticInit();
}
}
@Override
protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen)
throws IllegalBlockSizeException, BadPaddingException {
int maxSize = engineGetOutputSize(inputLen);
byte[] output = new byte[maxSize];
int finalSize;
try {
finalSize = doFinal(input, inputOffset, inputLen, output, 0);
} catch (ShortBufferException e) {
// This shouldn't be possible rethrow as RuntimeException
throw new RuntimeException("Short buffer exception shouldn't be possible from here.");
}
if ( maxSize == finalSize ) {
return output;
} else {
// TODO: Special doFinal to avoid this copy
byte[] exact = new byte[finalSize];
System.arraycopy(output, 0, exact, 0, finalSize);
return exact;
}
}
@Override
protected int engineDoFinal(byte[] input, int inputOffset, int inputLen,
byte[] output, int outputOffset) throws ShortBufferException,
IllegalBlockSizeException, BadPaddingException {
int result = doFinal(input, inputOffset, inputLen, output, outputOffset);
try {
finalSize = doFinal(input, inputOffset, inputLen, output, 0);
} catch (ShortBufferException e) {
// This shouldn't be possible rethrow as RuntimeException
throw new RuntimeException("Short buffer exception shouldn't be possible from here.");
}
if ( maxSize == finalSize ) {
return output;
} else {
// TODO: Special doFinal to avoid this copy
byte[] exact = new byte[finalSize];
System.arraycopy(output, 0, exact, 0, finalSize);
return exact;
}
}
@Override
protected int engineDoFinal(byte[] input, int inputOffset, int inputLen,
byte[] output, int outputOffset) throws ShortBufferException,
IllegalBlockSizeException, BadPaddingException {
int result = doFinal(input, inputOffset, inputLen, output, outputOffset);
if ( result == -1 ) {
throw new ShortBufferException();
}
return result;
}
private int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset)
throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
int outputSize = engineGetOutputSize(inputLen);
int updateAmt;
if (input != null && inputLen > 0) {
updateAmt = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
} else {
updateAmt = 0;
}
int finalAmt = nFinal(mCtxPtr, mPadding, output, outputOffset + updateAmt, outputSize - updateAmt);
int out = updateAmt + finalAmt;
return out;
}
private native int nFinal(long ctxPtr, boolean usePadding, byte[] output, int outputOffest, int outputSize)
throws ShortBufferException, IllegalBlockSizeException, BadPaddingException;
@Override
protected int engineGetBlockSize() {
return AES_BLOCK_SIZE;
}
@Override
protected byte[] engineGetIV() {
byte[] copyIV = new byte[0];
if (mIV != null) {
int lengthIV = mIV.length;
copyIV = new byte[lengthIV];
System.arraycopy(mIV, 0, copyIV, 0, lengthIV);
}
return copyIV;
}
@Override
protected int engineGetOutputSize(int inputLen) {
return inputLen + nGetCacheSize(mCtxPtr) + AES_BLOCK_SIZE;
}
private native int nGetCacheSize(long ctxPtr);
@Override
protected AlgorithmParameters engineGetParameters() {
// TODO Auto-generated method stub
return null;
}
@Override
protected void engineInit(int opmode, Key key, SecureRandom random)
throws InvalidKeyException {
byte[] ivArray = new byte[16];
random.nextBytes(ivArray);
init(opmode, key, new IvParameterSpec(ivArray));
}
@Override
protected void engineInit(int opmode, Key key,
AlgorithmParameterSpec params, SecureRandom random)
throws InvalidKeyException, InvalidAlgorithmParameterException {
if ( result == -1 ) {
throw new ShortBufferException();
}
return result;
}
private int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset)
throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
int outputSize = engineGetOutputSize(inputLen);
int updateAmt;
if (input != null && inputLen > 0) {
updateAmt = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
} else {
updateAmt = 0;
}
int finalAmt = nFinal(mCtxPtr, mPadding, output, outputOffset + updateAmt, outputSize - updateAmt);
int out = updateAmt + finalAmt;
return out;
}
private native int nFinal(long ctxPtr, boolean usePadding, byte[] output, int outputOffest, int outputSize)
throws ShortBufferException, IllegalBlockSizeException, BadPaddingException;
@Override
protected int engineGetBlockSize() {
return AES_BLOCK_SIZE;
}
@Override
protected byte[] engineGetIV() {
byte[] copyIV = new byte[0];
if (mIV != null) {
int lengthIV = mIV.length;
copyIV = new byte[lengthIV];
System.arraycopy(mIV, 0, copyIV, 0, lengthIV);
}
return copyIV;
}
@Override
protected int engineGetOutputSize(int inputLen) {
return inputLen + nGetCacheSize(mCtxPtr) + AES_BLOCK_SIZE;
}
private native int nGetCacheSize(long ctxPtr);
@Override
protected AlgorithmParameters engineGetParameters() {
// TODO Auto-generated method stub
return null;
}
@Override
protected void engineInit(int opmode, Key key, SecureRandom random)
throws InvalidKeyException {
byte[] ivArray = new byte[16];
random.nextBytes(ivArray);
init(opmode, key, new IvParameterSpec(ivArray));
}
@Override
protected void engineInit(int opmode, Key key,
AlgorithmParameterSpec params, SecureRandom random)
throws InvalidKeyException, InvalidAlgorithmParameterException {
IvParameterSpec ivparam;
IvParameterSpec ivparam;
if ( params instanceof IvParameterSpec ) {
ivparam = (IvParameterSpec) params;
} else {
throw new InvalidAlgorithmParameterException("params must be an IvParameterSpec.");
}
if ( params instanceof IvParameterSpec ) {
ivparam = (IvParameterSpec) params;
} else {
throw new InvalidAlgorithmParameterException("params must be an IvParameterSpec.");
}
init(opmode, key, ivparam);
}
init(opmode, key, ivparam);
}
@Override
protected void engineInit(int opmode, Key key, AlgorithmParameters params,
SecureRandom random) throws InvalidKeyException,
InvalidAlgorithmParameterException {
@Override
protected void engineInit(int opmode, Key key, AlgorithmParameters params,
SecureRandom random) throws InvalidKeyException,
InvalidAlgorithmParameterException {
try {
engineInit(opmode, key, params.getParameterSpec(AlgorithmParameterSpec.class), random);
} catch (InvalidParameterSpecException e) {
throw new InvalidAlgorithmParameterException(e);
}
try {
engineInit(opmode, key, params.getParameterSpec(AlgorithmParameterSpec.class), random);
} catch (InvalidParameterSpecException e) {
throw new InvalidAlgorithmParameterException(e);
}
}
}
private void init(int opmode, Key key, IvParameterSpec params) {
if ( mIsInited ) {
// Do not allow multiple inits
assert(true);
throw new RuntimeException("Don't allow multiple inits");
} else {
NativeLib.init();
mIsInited = true;
}
private void init(int opmode, Key key, IvParameterSpec params) {
if ( mIsInited ) {
// Do not allow multiple inits
assert(true);
throw new RuntimeException("Don't allow multiple inits");
} else {
NativeLib.init();
mIsInited = true;
}
mIV = params.getIV();
mEncrypting = opmode == Cipher.ENCRYPT_MODE;
mCtxPtr = nInit(mEncrypting, key.getEncoded(), mIV);
addToCleanupQueue(this, mCtxPtr);
}
mIV = params.getIV();
mEncrypting = opmode == Cipher.ENCRYPT_MODE;
mCtxPtr = nInit(mEncrypting, key.getEncoded(), mIV);
addToCleanupQueue(this, mCtxPtr);
}
private native long nInit(boolean encrypting, byte[] key, byte[] iv);
private native long nInit(boolean encrypting, byte[] key, byte[] iv);
@Override
protected void engineSetMode(String mode) throws NoSuchAlgorithmException {
if ( ! mode.equals("CBC") ) {
throw new NoSuchAlgorithmException("This only supports CBC mode");
}
}
@Override
protected void engineSetMode(String mode) throws NoSuchAlgorithmException {
if ( ! mode.equals("CBC") ) {
throw new NoSuchAlgorithmException("This only supports CBC mode");
}
}
@Override
protected void engineSetPadding(String padding)
throws NoSuchPaddingException {
@Override
protected void engineSetPadding(String padding)
throws NoSuchPaddingException {
if ( ! mIsInited ) {
NativeLib.init();
}
if ( ! mIsInited ) {
NativeLib.init();
}
if ( padding.length() == 0 ) {
return;
}
if ( padding.length() == 0 ) {
return;
}
if ( ! padding.equals("PKCS5Padding") ) {
throw new NoSuchPaddingException("Only supports PKCS5Padding.");
}
if ( ! padding.equals("PKCS5Padding") ) {
throw new NoSuchPaddingException("Only supports PKCS5Padding.");
}
mPadding = true;
mPadding = true;
}
}
@Override
protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
int maxSize = engineGetOutputSize(inputLen);
byte output[] = new byte[maxSize];
@Override
protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
int maxSize = engineGetOutputSize(inputLen);
byte output[] = new byte[maxSize];
int updateSize = update(input, inputOffset, inputLen, output, 0);
int updateSize = update(input, inputOffset, inputLen, output, 0);
if ( updateSize == maxSize ) {
return output;
} else {
// TODO: We could optimize update for this case to avoid this extra copy
byte[] exact = new byte[updateSize];
System.arraycopy(output, 0, exact, 0, updateSize);
return exact;
}
if ( updateSize == maxSize ) {
return output;
} else {
// TODO: We could optimize update for this case to avoid this extra copy
byte[] exact = new byte[updateSize];
System.arraycopy(output, 0, exact, 0, updateSize);
return exact;
}
}
}
@Override
protected int engineUpdate(byte[] input, int inputOffset, int inputLen,
byte[] output, int outputOffset) throws ShortBufferException {
@Override
protected int engineUpdate(byte[] input, int inputOffset, int inputLen,
byte[] output, int outputOffset) throws ShortBufferException {
int result = update(input, inputOffset, inputLen, output, outputOffset);
int result = update(input, inputOffset, inputLen, output, outputOffset);
if ( result == -1 ) {
throw new ShortBufferException("Insufficient buffer.");
}
if ( result == -1 ) {
throw new ShortBufferException("Insufficient buffer.");
}
return result;
return result;
}
}
int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) {
int outputSize = engineGetOutputSize(inputLen);
int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) {
int outputSize = engineGetOutputSize(inputLen);
int out = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
int out = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
return out;
return out;
}
}
private native int nUpdate(long ctxPtr, byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset, int outputSize);
private native int nUpdate(long ctxPtr, byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset, int outputSize);
}

View File

@@ -20,27 +20,27 @@
package com.kunzisoft.keepass.crypto;
public class NativeLib {
private static boolean isLoaded = false;
private static boolean loadSuccess = false;
private static boolean isLoaded = false;
private static boolean loadSuccess = false;
public static boolean loaded() {
return init();
}
public static boolean loaded() {
return init();
}
public static boolean init() {
if ( ! isLoaded ) {
try {
System.loadLibrary("final-key");
System.loadLibrary("argon2");
} catch ( UnsatisfiedLinkError e) {
return false;
}
isLoaded = true;
loadSuccess = true;
}
public static boolean init() {
if ( ! isLoaded ) {
try {
System.loadLibrary("final-key");
System.loadLibrary("argon2");
} catch ( UnsatisfiedLinkError e) {
return false;
}
isLoaded = true;
loadSuccess = true;
}
return loadSuccess;
return loadSuccess;
}
}
}

View File

@@ -26,48 +26,48 @@ import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;
public class PwStreamCipherFactory {
public static StreamCipher getInstance(CrsAlgorithm alg, byte[] key) {
if ( alg == CrsAlgorithm.Salsa20 ) {
return getSalsa20(key);
} else if (alg == CrsAlgorithm.ChaCha20) {
return getChaCha20(key);
} else {
return null;
}
}
public static StreamCipher getInstance(CrsAlgorithm alg, byte[] key) {
if ( alg == CrsAlgorithm.Salsa20 ) {
return getSalsa20(key);
} else if (alg == CrsAlgorithm.ChaCha20) {
return getChaCha20(key);
} else {
return null;
}
}
private static final byte[] SALSA_IV = new byte[]{ (byte)0xE8, 0x30, 0x09, 0x4B,
private static final byte[] SALSA_IV = new byte[]{ (byte)0xE8, 0x30, 0x09, 0x4B,
(byte)0x97, 0x20, 0x5D, 0x2A };
private static StreamCipher getSalsa20(byte[] key) {
// Build stream cipher key
byte[] key32 = CryptoUtil.hashSha256(key);
private static StreamCipher getSalsa20(byte[] key) {
// Build stream cipher key
byte[] key32 = CryptoUtil.hashSha256(key);
KeyParameter keyParam = new KeyParameter(key32);
ParametersWithIV ivParam = new ParametersWithIV(keyParam, SALSA_IV);
KeyParameter keyParam = new KeyParameter(key32);
ParametersWithIV ivParam = new ParametersWithIV(keyParam, SALSA_IV);
StreamCipher cipher = new Salsa20Engine();
cipher.init(true, ivParam);
StreamCipher cipher = new Salsa20Engine();
cipher.init(true, ivParam);
return cipher;
}
return cipher;
}
private static StreamCipher getChaCha20(byte[] key) {
// Build stream cipher key
byte[] hash = CryptoUtil.hashSha512(key);
byte[] key32 = new byte[32];
byte[] iv = new byte[12];
private static StreamCipher getChaCha20(byte[] key) {
// Build stream cipher key
byte[] hash = CryptoUtil.hashSha512(key);
byte[] key32 = new byte[32];
byte[] iv = new byte[12];
System.arraycopy(hash, 0, key32, 0, 32);
System.arraycopy(hash, 0, key32, 0, 32);
System.arraycopy(hash, 32, iv, 0, 12);
KeyParameter keyParam = new KeyParameter(key32);
ParametersWithIV ivParam = new ParametersWithIV(keyParam, iv);
KeyParameter keyParam = new KeyParameter(key32);
ParametersWithIV ivParam = new ParametersWithIV(keyParam, iv);
StreamCipher cipher = new ChaCha7539Engine();
cipher.init(true, ivParam);
cipher.init(true, ivParam);
return cipher;
}
return cipher;
}
}

View File

@@ -31,48 +31,48 @@ import javax.crypto.spec.SecretKeySpec;
public class AndroidFinalKey extends FinalKey {
@Override
public byte[] transformMasterKey(byte[] pKeySeed, byte[] pKey, long rounds) throws IOException {
Cipher cipher;
try {
cipher = Cipher.getInstance("AES/ECB/NoPadding");
} catch (NoSuchAlgorithmException e) {
throw new IOException("NoSuchAlgorithm: " + e.getMessage());
} catch (NoSuchPaddingException e) {
throw new IOException("NoSuchPadding: " + e.getMessage());
}
@Override
public byte[] transformMasterKey(byte[] pKeySeed, byte[] pKey, long rounds) throws IOException {
Cipher cipher;
try {
cipher = Cipher.getInstance("AES/ECB/NoPadding");
} catch (NoSuchAlgorithmException e) {
throw new IOException("NoSuchAlgorithm: " + e.getMessage());
} catch (NoSuchPaddingException e) {
throw new IOException("NoSuchPadding: " + e.getMessage());
}
try {
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(pKeySeed, "AES"));
} catch (InvalidKeyException e) {
throw new IOException("InvalidPasswordException: " + e.getMessage());
}
try {
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(pKeySeed, "AES"));
} catch (InvalidKeyException e) {
throw new IOException("InvalidPasswordException: " + e.getMessage());
}
// Encrypt key rounds times
byte[] newKey = new byte[pKey.length];
System.arraycopy(pKey, 0, newKey, 0, pKey.length);
byte[] destKey = new byte[pKey.length];
for (int i = 0; i < rounds; i++) {
try {
cipher.update(newKey, 0, newKey.length, destKey, 0);
System.arraycopy(destKey, 0, newKey, 0, newKey.length);
// Encrypt key rounds times
byte[] newKey = new byte[pKey.length];
System.arraycopy(pKey, 0, newKey, 0, pKey.length);
byte[] destKey = new byte[pKey.length];
for (int i = 0; i < rounds; i++) {
try {
cipher.update(newKey, 0, newKey.length, destKey, 0);
System.arraycopy(destKey, 0, newKey, 0, newKey.length);
} catch (ShortBufferException e) {
throw new IOException("Short buffer: " + e.getMessage());
}
}
} catch (ShortBufferException e) {
throw new IOException("Short buffer: " + e.getMessage());
}
}
// Hash the key
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
assert true;
throw new IOException("SHA-256 not implemented here: " + e.getMessage());
}
// Hash the key
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
assert true;
throw new IOException("SHA-256 not implemented here: " + e.getMessage());
}
md.update(newKey);
return md.digest();
}
md.update(newKey);
return md.digest();
}
}

View File

@@ -22,5 +22,5 @@ package com.kunzisoft.keepass.crypto.finalkey;
import java.io.IOException;
public abstract class FinalKey {
public abstract byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException;
public abstract byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException;
}

View File

@@ -22,17 +22,17 @@ package com.kunzisoft.keepass.crypto.finalkey;
import com.kunzisoft.keepass.crypto.CipherFactory;
public class FinalKeyFactory {
public static FinalKey createFinalKey() {
return createFinalKey(false);
}
public static FinalKey createFinalKey() {
return createFinalKey(false);
}
public static FinalKey createFinalKey(boolean androidOverride) {
// Prefer the native final key implementation
if ( !CipherFactory.deviceBlacklisted() && !androidOverride && NativeFinalKey.availble() ) {
return new NativeFinalKey();
} else {
// Fall back on the android crypto implementation
return new AndroidFinalKey();
}
}
public static FinalKey createFinalKey(boolean androidOverride) {
// Prefer the native final key implementation
if ( !CipherFactory.deviceBlacklisted() && !androidOverride && NativeFinalKey.availble() ) {
return new NativeFinalKey();
} else {
// Fall back on the android crypto implementation
return new AndroidFinalKey();
}
}
}

View File

@@ -26,21 +26,21 @@ import java.io.IOException;
public class NativeFinalKey extends FinalKey {
public static boolean availble() {
return NativeLib.init();
}
public static boolean availble() {
return NativeLib.init();
}
@Override
public byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException {
NativeLib.init();
@Override
public byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException {
NativeLib.init();
return nTransformMasterKey(seed, key, rounds);
return nTransformMasterKey(seed, key, rounds);
}
}
private static native byte[] nTransformMasterKey(byte[] seed, byte[] key, long rounds);
private static native byte[] nTransformMasterKey(byte[] seed, byte[] key, long rounds);
// For testing
// For testing
/*
public static byte[] reflect(byte[] key) {
NativeLib.init();

View File

@@ -19,7 +19,7 @@
*/
package com.kunzisoft.keepass.crypto.keyDerivation;
import com.kunzisoft.keepass.collections.VariantDictionary;
import com.kunzisoft.keepass.utils.VariantDictionary;
import com.kunzisoft.keepass.stream.LEDataInputStream;
import com.kunzisoft.keepass.stream.LEDataOutputStream;
import com.kunzisoft.keepass.utils.Types;

View File

@@ -111,7 +111,7 @@ enum class SortNodeEnum {
object1,
object2,
object1.creationTime.date
.compareTo(object2.creationTime.date))
?.compareTo(object2.creationTime.date) ?: 0)
}
}
@@ -128,7 +128,7 @@ enum class SortNodeEnum {
object1,
object2,
object1.lastModificationTime.date
.compareTo(object2.lastModificationTime.date))
?.compareTo(object2.lastModificationTime.date) ?: 0)
}
}
@@ -145,7 +145,7 @@ enum class SortNodeEnum {
object1,
object2,
object1.lastAccessTime.date
.compareTo(object2.lastAccessTime.date))
?.compareTo(object2.lastAccessTime.date) ?: 0)
}
}
@@ -186,7 +186,7 @@ enum class SortNodeEnum {
return 0
val groupCreationComp = object1.creationTime.date
.compareTo(object2.creationTime.date)
?.compareTo(object2.creationTime.date) ?: 0
// If same creation, can be different
return if (groupCreationComp == 0) {
object1.hashCode() - object2.hashCode()
@@ -205,7 +205,7 @@ enum class SortNodeEnum {
return 0
val groupLastModificationComp = object1.lastModificationTime.date
.compareTo(object2.lastModificationTime.date)
?.compareTo(object2.lastModificationTime.date) ?: 0
// If same creation, can be different
return if (groupLastModificationComp == 0) {
object1.hashCode() - object2.hashCode()
@@ -224,7 +224,7 @@ enum class SortNodeEnum {
return 0
val groupLastAccessComp = object1.lastAccessTime.date
.compareTo(object2.lastAccessTime.date)
?.compareTo(object2.lastAccessTime.date) ?: 0
// If same creation, can be different
return if (groupLastAccessComp == 0) {
object1.hashCode() - object2.hashCode()
@@ -261,7 +261,7 @@ enum class SortNodeEnum {
return 0
val entryCreationComp = object1.creationTime.date
.compareTo(object2.creationTime.date)
?.compareTo(object2.creationTime.date) ?: 0
// If same creation, can be different
return if (entryCreationComp == 0) {
object1.hashCode() - object2.hashCode()
@@ -280,7 +280,7 @@ enum class SortNodeEnum {
return 0
val entryLastModificationComp = object1.lastModificationTime.date
.compareTo(object2.lastModificationTime.date)
?.compareTo(object2.lastModificationTime.date) ?: 0
// If same creation, can be different
return if (entryLastModificationComp == 0) {
object1.hashCode() - object2.hashCode()
@@ -299,7 +299,7 @@ enum class SortNodeEnum {
return 0
val entryLastAccessComp = object1.lastAccessTime.date
.compareTo(object2.lastAccessTime.date)
?.compareTo(object2.lastAccessTime.date) ?: 0
// If same creation, can be different
return if (entryLastAccessComp == 0) {
object1.hashCode() - object2.hashCode()

View File

@@ -28,15 +28,15 @@ import com.kunzisoft.keepass.utils.getUriInputStream
import java.io.IOException
class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
ctx: Context,
db: Database,
context: Context,
database: Database,
withMasterPassword: Boolean,
masterPassword: String?,
withKeyFile: Boolean,
keyFile: Uri?,
actionRunnable: ActionRunnable? = null,
save: Boolean)
: SaveDatabaseRunnable(ctx, db, actionRunnable, save) {
save: Boolean,
actionRunnable: ActionRunnable? = null)
: SaveDatabaseRunnable(context, database, save, actionRunnable) {
private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null

View File

@@ -27,14 +27,11 @@ class CreateDatabaseRunnable(private val mFilename: String,
val onDatabaseCreate: (database: Database) -> ActionRunnable)
: ActionRunnable() {
var database: Database? = null
override fun run() {
try {
// Create new database record
database = Database(mFilename)
App.setDB(database)
database?.apply {
Database(mFilename).apply {
App.currentDatabase = this
// Set Database state
loaded = true
// Commit changes

View File

@@ -25,31 +25,40 @@ import android.preference.PreferenceManager
import android.support.annotation.StringRes
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.FileNotFoundException
import java.io.IOException
import java.lang.ref.WeakReference
class LoadDatabaseRunnable(private val mContext: Context,
class LoadDatabaseRunnable(private val mWeakContext: WeakReference<Context>,
private val mDatabase: Database,
private val mUri: Uri,
private val mPass: String?,
private val mKey: Uri?,
private val progressTaskUpdater: ProgressTaskUpdater,
private val progressTaskUpdater: ProgressTaskUpdater?,
nestedAction: ActionRunnable)
: ActionRunnable(nestedAction, executeNestedActionIfResultFalse = true) {
private val mRememberKeyFile: Boolean = PreferenceManager.getDefaultSharedPreferences(mContext)
.getBoolean(mContext.getString(R.string.keyfile_key), mContext.resources.getBoolean(R.bool.keyfile_default))
private val mRememberKeyFile: Boolean
get() {
return mWeakContext.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it)
.getBoolean(it.getString(R.string.keyfile_key),
it.resources.getBoolean(R.bool.keyfile_default))
} ?: true
}
override fun run() {
try {
mDatabase.loadData(mContext, mUri, mPass, mKey, progressTaskUpdater)
saveFileData(mUri, mKey)
finishRun(true)
mWeakContext.get()?.let {
mDatabase.loadData(it, mUri, mPass, mKey, progressTaskUpdater)
saveFileData(mUri, mKey)
finishRun(true)
} ?: finishRun(false, "Context null")
} catch (e: ArcFourException) {
catchError(e, R.string.error_arc4)
return
@@ -98,7 +107,7 @@ class LoadDatabaseRunnable(private val mContext: Context,
}
private fun catchError(e: Throwable, @StringRes messageId: Int, addThrowableMessage: Boolean = false) {
var errorMessage = mContext.getString(messageId)
var errorMessage = mWeakContext.get()?.getString(messageId)
Log.e(TAG, errorMessage, e)
if (addThrowableMessage)
errorMessage = errorMessage + " " + e.localizedMessage
@@ -110,7 +119,7 @@ class LoadDatabaseRunnable(private val mContext: Context,
if (!mRememberKeyFile) {
keyFileUri = null
}
App.getFileHistory().createFile(uri, keyFileUri)
FileDatabaseHistory.getInstance(mWeakContext).addDatabaseUri(uri, keyFileUri)
}
override fun onFinishRun(isSuccess: Boolean, message: String?) {}

View File

@@ -1,28 +0,0 @@
package com.kunzisoft.keepass.database.action
import android.support.v4.app.FragmentActivity
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
class ProgressDialogRunnable(val context: FragmentActivity,
private val titleId: Int,
private val actionRunnable: (ProgressTaskUpdater)-> ActionRunnable)
: ActionRunnable() {
override fun run() {
// Show the dialog
val progressTaskUpdater: ProgressTaskUpdater = ProgressTaskDialogFragment.start(
context.supportFragmentManager,
titleId)
// Do the action
actionRunnable.invoke(progressTaskUpdater).run()
super.run()
}
override fun onFinishRun(isSuccess: Boolean, message: String?) {
// Remove the progress task
ProgressTaskDialogFragment.stop(context)
}
}

View File

@@ -0,0 +1,14 @@
package com.kunzisoft.keepass.database.action
import android.support.v4.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
class ProgressDialogSaveDatabaseThread(activity: FragmentActivity,
actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable)
: ProgressDialogThread(activity,
actionRunnable,
R.string.saving_database,
null,
R.string.do_not_kill_app)

View File

@@ -0,0 +1,64 @@
package com.kunzisoft.keepass.database.action
import android.os.AsyncTask
import android.support.annotation.StringRes
import android.support.v4.app.FragmentActivity
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
open class ProgressDialogThread(private val activity: FragmentActivity,
private val actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable,
@StringRes private val titleId: Int,
@StringRes private val messageId: Int? = null,
@StringRes private val warningId: Int? = null) {
private val progressTaskDialogFragment = ProgressTaskDialogFragment.build(
titleId,
messageId,
warningId)
private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null
init {
actionRunnableAsyncTask = ActionRunnableAsyncTask(progressTaskDialogFragment,
{
activity.runOnUiThread {
// Show the dialog
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
}
}, {
activity.runOnUiThread {
// Remove the progress task
ProgressTaskDialogFragment.stop(activity)
}
})
}
fun start() {
actionRunnableAsyncTask?.execute(actionRunnable)
}
private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater,
private val onPreExecute: () -> Unit,
private val onPostExecute: () -> Unit)
: AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, Void>() {
override fun onPreExecute() {
super.onPreExecute()
onPreExecute.invoke()
}
override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): Void? {
actionRunnables.forEach {
it?.invoke(progressTaskUpdater)?.run()
}
return null
}
override fun onPostExecute(result: Void?) {
super.onPostExecute(result)
onPostExecute.invoke()
}
}
}

View File

@@ -1,59 +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.database.action
import android.support.v4.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
open class SaveDatabaseProgressDialogRunnable(private var contextDatabase: FragmentActivity,
database: Database,
private val actionRunnable: ((ProgressTaskUpdater)-> ActionRunnable)?,
save: Boolean):
SaveDatabaseRunnable(contextDatabase, database, null, save) {
override fun run() {
// Show the dialog
val progressTaskUpdater : ProgressTaskUpdater = ProgressTaskDialogFragment.start(
contextDatabase.supportFragmentManager,
R.string.saving_database,
null,
R.string.do_not_kill_app)
// Do the action if defined
actionRunnable?.invoke(progressTaskUpdater)?.run()
// Save the database
super.run()
// Call the finish function to close the dialog
finishRun(true)
}
override fun onFinishRun(isSuccess: Boolean, message: String?) {
super.onFinishRun(isSuccess, message)
// Remove the progress task
ProgressTaskDialogFragment.stop(contextDatabase)
}
}

View File

@@ -28,19 +28,20 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper
import java.io.IOException
abstract class SaveDatabaseRunnable(protected var context: Context,
protected var database: Database,
nestedAction: ActionRunnable? = null,
private val save: Boolean) : ActionRunnable(nestedAction) {
open class SaveDatabaseRunnable(protected var context: Context,
protected var database: Database,
private val save: Boolean,
nestedAction: ActionRunnable? = null) : ActionRunnable(nestedAction) {
init {
TimeoutHelper.temporarilyDisableTimeout()
}
// TODO Service to prevent background thread kill
override fun run() {
if (save) {
try {
database.saveData(context)
database.saveData(context.contentResolver)
} catch (e: IOException) {
finishRun(false, e.message)
} catch (e: PwDbOutputException) {
@@ -53,7 +54,6 @@ abstract class SaveDatabaseRunnable(protected var context: Context,
override fun onFinishRun(isSuccess: Boolean, message: String?) {
// Need to call super.onFinishRun(isSuccess, message) in child class
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(context)
}
}

View File

@@ -1,15 +1,15 @@
package com.kunzisoft.keepass.database.action.node
import android.support.v4.app.FragmentActivity
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.action.SaveDatabaseProgressDialogRunnable
abstract class ActionNodeDatabaseRunnable(
context: FragmentActivity,
database: Database,
private val callbackRunnable: AfterActionNodeFinishRunnable?,
save: Boolean)
: SaveDatabaseProgressDialogRunnable(context, database, null, save) {
: SaveDatabaseRunnable(context, database, save) {
/**
* Function do to a node action, don't implements run() if used this

View File

@@ -34,8 +34,8 @@ class AddEntryRunnable constructor(
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
override fun nodeAction() {
mNewEntry.touch(true, true)
mParent.touch(true, true)
mNewEntry.touch(modified = true, touchParents = true)
mParent.touch(modified = true, touchParents = true)
database.addEntryTo(mNewEntry, mParent)
}

View File

@@ -33,8 +33,8 @@ class AddGroupRunnable constructor(
: ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) {
override fun nodeAction() {
mNewGroup.touch(true, true)
mParent.touch(true, true)
mNewGroup.touch(modified = true, touchParents = true)
mParent.touch(modified = true, touchParents = true)
database.addGroupTo(mNewGroup, mParent)
}

Some files were not shown because too many files have changed in this diff Show More