diff --git a/CHANGELOG b/CHANGELOG index 00d67607d..2612ef808 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +KeepassDX (2.5.0.0beta23) + * New, more secure database creation workflow + * Recognize more database files + * Add alias for history files (WARNING: history is erased) + * New Biometric unlock (Fingerprint with new API) + * Fix entry references + * Fix OOM with KeyFile + * Fix small issues + KeepassDX (2.5.0.0beta22) * Rebuild code for actions * Add UUID as entry view diff --git a/app/.gitignore b/app/.gitignore index 3cfb122fc..04d6aa0d6 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,2 @@ +.cxx .externalNativeBuild diff --git a/app/build.gradle b/app/build.gradle index a3480f342..4e3978343 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,15 +4,15 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 27 + compileSdkVersion 28 buildToolsVersion '28.0.3' defaultConfig { applicationId "com.kunzisoft.keepass" minSdkVersion 14 - targetSdkVersion 27 - versionCode = 22 - versionName = "2.5.0.0beta22" + targetSdkVersion 28 + versionCode = 23 + versionName = "2.5.0.0beta23" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" @@ -79,42 +79,35 @@ android { } } -def supportVersion = "27.1.1" def spongycastleVersion = "1.58.0.0" -def permissionDispatcherVersion = "3.3.1" +def room_version = "2.1.0" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "com.android.support:appcompat-v7:$supportVersion" - implementation "com.android.support:design:$supportVersion" - implementation "com.android.support:preference-v7:$supportVersion" - implementation "com.android.support:preference-v14:$supportVersion" - implementation "com.android.support:cardview-v7:$supportVersion" - implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.preference:preference:1.1.0' + implementation 'androidx.legacy:legacy-preference-v14:1.0.0' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.biometric:biometric:1.0.0-beta01' + implementation 'com.google.android.material:material:1.0.0' + + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + implementation "com.madgag.spongycastle:core:$spongycastleVersion" implementation "com.madgag.spongycastle:prov:$spongycastleVersion" // Expandable view implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2' // Time implementation 'joda-time:joda-time:2.9.9' - implementation 'org.sufficientlysecure:html-textview:3.5' - implementation 'com.nononsenseapps:filepicker:4.1.0' + // Education implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0' - // Permissions - implementation("com.github.hotchemi:permissionsdispatcher:$permissionDispatcherVersion") { - // if you don't use android.app.Fragment you can exclude support for them - exclude module: "support-v13" - } - 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' // Base64 implementation 'biz.source_code:base64coder:2010-12-19' - // IO-Extras - implementation 'com.github.davidmoten:io-extras:0.1' - implementation 'com.google.code.gson:gson:2.8.4' - implementation 'com.google.guava:guava:23.0-android' // Icon pack implementation project(path: ':icon-pack-classic') implementation project(path: ':icon-pack-material') diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/AccentTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/AccentTest.java deleted file mode 100644 index 20b1a5d83..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/AccentTest.java +++ /dev/null @@ -1,44 +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 . - * - */ -package com.kunzisoft.keepass.tests; - -import android.test.AndroidTestCase; - -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"; - - public void testOpen() { - - /* - try { - TestData.GetDb(getContext(), ASSET, PASSWORD, KEYFILE, FILENAME); - } catch (Exception e) { - assertTrue("Failed to open database", false); - } - */ - } - -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/AllTests.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/AllTests.java deleted file mode 100644 index 434cf5011..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/AllTests.java +++ /dev/null @@ -1,34 +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 . - * - */ -package com.kunzisoft.keepass.tests; - -import junit.framework.Test; -import junit.framework.TestSuite; - -import android.test.suitebuilder.TestSuiteBuilder; - -public class AllTests extends TestSuite { - - public static Test suite() { - return new TestSuiteBuilder(AllTests.class) - .includeAllPackagesUnderHere() - .build(); - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/OutputTests.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/OutputTests.java deleted file mode 100644 index dde340c2b..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/OutputTests.java +++ /dev/null @@ -1,35 +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 . - * - */ -package com.kunzisoft.keepass.tests; - -import junit.framework.Test; -import junit.framework.TestSuite; - -import android.test.suitebuilder.TestSuiteBuilder; - -public class OutputTests extends TestSuite { - - public static Test suite() { - - return new TestSuiteBuilder(AllTests.class) - .includePackages("com.kunzisoft.keepass.tests.output") - .build(); - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV3.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV3.java deleted file mode 100644 index 8ec7691f4..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV3.java +++ /dev/null @@ -1,63 +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 . - * - */ -package com.kunzisoft.keepass.tests; - -import static org.junit.Assert.assertArrayEquals; - -import java.io.UnsupportedEncodingException; -import java.util.Calendar; - - -import android.test.AndroidTestCase; - -import com.kunzisoft.keepass.database.element.PwEntryV3; -import com.kunzisoft.keepass.tests.database.TestData; - -public class PwEntryTestV3 extends AndroidTestCase { - PwEntryV3 mPE; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - // mPE = (PwEntryV3) TestData.GetTest1(getContext()).getEntryAt(0); - - } - - 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"); - - assertArrayEquals(password, mPE.getPasswordBytes()); - } - - 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); - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV4.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV4.java deleted file mode 100644 index ab1b7737f..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV4.java +++ /dev/null @@ -1,59 +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 . - * - */ -package com.kunzisoft.keepass.tests; - -import junit.framework.TestCase; - -public class PwEntryTestV4 extends TestCase { - public void testAssign() { - /* - TODO Test - PwEntryV4 entry = new PwEntryV4(); - - entry.setAdditional("test223"); - - entry.setAutoType(new AutoType()); - entry.getAutoType().defaultSequence = "1324"; - entry.getAutoType().enabled = true; - entry.getAutoType().obfuscationOptions = 123412432109L; - entry.getAutoType().put("key", "value"); - - entry.setBackgroundColor("blue"); - entry.putProtectedBinary("key1", new ProtectedBinary(false, new byte[] {0,1})); - entry.setIconCustom(new PwIconCustom(UUID.randomUUID(), new byte[0])); - entry.setForegroundColor("red"); - entry.addToHistory(new PwEntryV4()); - entry.setIconStandard(new PwIconStandard(5)); - entry.setOverrideURL("override"); - entry.setParent(new PwGroupV4()); - entry.addExtraField("key2", new ProtectedString(false, "value2")); - entry.setUrl("http://localhost"); - entry.setNodeId(UUID.randomUUID()); - - PwEntryV4 target = new PwEntryV4(); - target.updateWith(entry); - - /* This test is not so useful now that I am not implementing value equality for Entries - assertTrue("Entries do not match.", entry.equals(target)); - */ - - } - -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwGroupTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwGroupTest.java deleted file mode 100644 index 097b2f294..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwGroupTest.java +++ /dev/null @@ -1,44 +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 . - * - */ -package com.kunzisoft.keepass.tests; - - -import android.test.AndroidTestCase; - -import com.kunzisoft.keepass.database.element.PwGroupV3; -import com.kunzisoft.keepass.tests.database.TestData; - -public class PwGroupTest extends AndroidTestCase { - - PwGroupV3 mPG; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - //mPG = (PwGroupV3) TestData.GetTest1(getContext()).getGroups().get(0); - - } - - public void testGroupName() { - //assertTrue("Name was " + mPG.getTitle(), mPG.getTitle().equals("Internet")); - } -} - diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/TestUtil.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/TestUtil.java deleted file mode 100644 index 542f69e4b..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/TestUtil.java +++ /dev/null @@ -1,56 +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 . - * - */ -package com.kunzisoft.keepass.tests; - -import android.content.Context; -import android.content.res.AssetManager; -import android.os.Environment; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; - -public class TestUtil { - private static final File sdcard = Environment.getExternalStorageDirectory(); - - public static void extractKey(Context ctx, String asset, String target) throws Exception { - - 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); - } - } - - keyFile.close(); - - } - - public static String getSdPath(String filename) { - File file = new File(sdcard, filename); - return file.getAbsolutePath(); - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.java deleted file mode 100644 index 66f792b8b..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.java +++ /dev/null @@ -1,211 +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 . - * - */ -package com.kunzisoft.keepass.tests; - -import static org.junit.Assert.assertArrayEquals; - -import java.io.ByteArrayOutputStream; -import java.util.Calendar; -import java.util.Random; -import java.util.UUID; - -import junit.framework.TestCase; - -import com.kunzisoft.keepass.database.element.PwDate; -import com.kunzisoft.keepass.stream.LEDataInputStream; -import com.kunzisoft.keepass.stream.LEDataOutputStream; -import com.kunzisoft.keepass.utils.Types; - -public class TypesTest extends TestCase { - - public void testReadWriteLongZero() { - testReadWriteLong((byte) 0); - } - - public void testReadWriteLongMax() { - testReadWriteLong(Byte.MAX_VALUE); - } - - public void testReadWriteLongMin() { - testReadWriteLong(Byte.MIN_VALUE); - } - - public void testReadWriteLongRnd() { - Random rnd = new Random(); - byte[] buf = new byte[1]; - rnd.nextBytes(buf); - - testReadWriteLong(buf[0]); - } - - private void testReadWriteLong(byte value) { - byte[] orig = new byte[8]; - byte[] dest = new byte[8]; - - setArray(orig, value, 0, 8); - - long one = LEDataInputStream.readLong(orig, 0); - LEDataOutputStream.writeLong(one, dest, 0); - - assertArrayEquals(orig, dest); - - } - - public void testReadWriteIntZero() { - testReadWriteInt((byte) 0); - } - - public void testReadWriteIntMin() { - testReadWriteInt(Byte.MIN_VALUE); - } - - public void testReadWriteIntMax() { - testReadWriteInt(Byte.MAX_VALUE); - } - - 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; - } - - setArray(orig, value, 0, 4); - - int one = LEDataInputStream.readInt(orig, 0); - - LEDataOutputStream.writeInt(one, dest, 0); - - 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; - } - } - - public void testReadWriteShortOne() { - byte[] orig = new byte[2]; - byte[] dest = new byte[2]; - - orig[0] = 0; - orig[1] = 1; - - int one = LEDataInputStream.readUShort(orig, 0); - dest = LEDataOutputStream.writeUShortBuf(one); - - assertArrayEquals(orig, dest); - - } - - public void testReadWriteShortMin() { - testReadWriteShort(Byte.MIN_VALUE); - } - - public void testReadWriteShortMax() { - testReadWriteShort(Byte.MAX_VALUE); - } - - private void testReadWriteShort(byte value) { - byte[] orig = new byte[2]; - byte[] dest = new byte[2]; - - setArray(orig, value, 0, 2); - - int one = LEDataInputStream.readUShort(orig, 0); - LEDataOutputStream.writeUShort(one, dest, 0); - - assertArrayEquals(orig, dest); - - } - - public void testReadWriteByteZero() { - testReadWriteByte((byte) 0); - } - - public void testReadWriteByteMin() { - testReadWriteByte(Byte.MIN_VALUE); - } - - public void testReadWriteByteMax() { - testReadWriteShort(Byte.MAX_VALUE); - } - - private void testReadWriteByte(byte value) { - byte[] orig = new byte[1]; - byte[] dest = new byte[1]; - - setArray(orig, value, 0, 1); - - int one = Types.readUByte(orig, 0); - Types.writeUByte(one, dest, 0); - - assertArrayEquals(orig, dest); - - } - - public void testDate() { - Calendar cal = Calendar.getInstance(); - - Calendar expected = Calendar.getInstance(); - expected.set(2008, 1, 2, 3, 4, 5); - - 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)); - } - - 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); - - 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; - } - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - LEDataOutputStream leos = new LEDataOutputStream(bos); - leos.writeLong(Types.ULONG_MAX_VALUE); - leos.close(); - - byte[] uLongMax = bos.toByteArray(); - - assertArrayEquals(ulongBytes, uLongMax); - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.kt b/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.kt new file mode 100644 index 000000000..5a353e858 --- /dev/null +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.kt @@ -0,0 +1,210 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.tests + +import org.junit.Assert.assertArrayEquals + +import java.io.ByteArrayOutputStream +import java.util.Calendar +import java.util.Random + +import junit.framework.TestCase + +import com.kunzisoft.keepass.database.element.PwDate +import com.kunzisoft.keepass.stream.LEDataInputStream +import com.kunzisoft.keepass.stream.LEDataOutputStream +import com.kunzisoft.keepass.utils.Types + +class TypesTest : TestCase() { + + fun testReadWriteLongZero() { + testReadWriteLong(0.toByte()) + } + + fun testReadWriteLongMax() { + testReadWriteLong(java.lang.Byte.MAX_VALUE) + } + + fun testReadWriteLongMin() { + testReadWriteLong(java.lang.Byte.MIN_VALUE) + } + + fun testReadWriteLongRnd() { + val rnd = Random() + val buf = ByteArray(1) + rnd.nextBytes(buf) + + testReadWriteLong(buf[0]) + } + + private fun testReadWriteLong(value: Byte) { + val orig = ByteArray(8) + val dest = ByteArray(8) + + setArray(orig, value, 0, 8) + + val one = LEDataInputStream.readLong(orig, 0) + LEDataOutputStream.writeLong(one, dest, 0) + + assertArrayEquals(orig, dest) + + } + + fun testReadWriteIntZero() { + testReadWriteInt(0.toByte()) + } + + fun testReadWriteIntMin() { + testReadWriteInt(java.lang.Byte.MIN_VALUE) + } + + fun testReadWriteIntMax() { + testReadWriteInt(java.lang.Byte.MAX_VALUE) + } + + private fun testReadWriteInt(value: Byte) { + val orig = ByteArray(4) + val dest = ByteArray(4) + + for (i in 0..3) { + orig[i] = 0 + } + + setArray(orig, value, 0, 4) + + val one = LEDataInputStream.readInt(orig, 0) + + LEDataOutputStream.writeInt(one, dest, 0) + + assertArrayEquals(orig, dest) + + } + + private fun setArray(buf: ByteArray, value: Byte, offset: Int, size: Int) { + for (i in offset until offset + size) { + buf[i] = value + } + } + + fun testReadWriteShortOne() { + val orig = ByteArray(2) + + orig[0] = 0 + orig[1] = 1 + + val one = LEDataInputStream.readUShort(orig, 0) + val dest = LEDataOutputStream.writeUShortBuf(one) + + assertArrayEquals(orig, dest) + + } + + fun testReadWriteShortMin() { + testReadWriteShort(java.lang.Byte.MIN_VALUE) + } + + fun testReadWriteShortMax() { + testReadWriteShort(java.lang.Byte.MAX_VALUE) + } + + private fun testReadWriteShort(value: Byte) { + val orig = ByteArray(2) + val dest = ByteArray(2) + + setArray(orig, value, 0, 2) + + val one = LEDataInputStream.readUShort(orig, 0) + LEDataOutputStream.writeUShort(one, dest, 0) + + assertArrayEquals(orig, dest) + + } + + fun testReadWriteByteZero() { + testReadWriteByte(0.toByte()) + } + + fun testReadWriteByteMin() { + testReadWriteByte(java.lang.Byte.MIN_VALUE) + } + + fun testReadWriteByteMax() { + testReadWriteShort(java.lang.Byte.MAX_VALUE) + } + + private fun testReadWriteByte(value: Byte) { + val orig = ByteArray(1) + val dest = ByteArray(1) + + setArray(orig, value, 0, 1) + + val one = Types.readUByte(orig, 0) + Types.writeUByte(one, dest, 0) + + assertArrayEquals(orig, dest) + + } + + fun testDate() { + val cal = Calendar.getInstance() + + val expected = Calendar.getInstance() + expected.set(2008, 1, 2, 3, 4, 5) + + val buf = PwDate.writeTime(expected.time, cal) + val actual = Calendar.getInstance() + actual.time = PwDate.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)) + } + + fun testUUID() { + val rnd = Random() + val bUUID = ByteArray(16) + rnd.nextBytes(bUUID) + + val uuid = Types.bytestoUUID(bUUID) + val eUUID = Types.UUIDtoBytes(uuid) + + assertArrayEquals("UUID match failed", bUUID, eUUID) + } + + @Throws(Exception::class) + fun testULongMax() { + val ulongBytes = ByteArray(8) + for (i in ulongBytes.indices) { + ulongBytes[i] = -1 + } + + val bos = ByteArrayOutputStream() + val leos = LEDataOutputStream(bos) + leos.writeLong(Types.ULONG_MAX_VALUE) + leos.close() + + val uLongMax = bos.toByteArray() + + assertArrayEquals(ulongBytes, uLongMax) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/AESTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/AESTest.java deleted file mode 100644 index b439b28a4..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/AESTest.java +++ /dev/null @@ -1,83 +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 . - * - */ -package com.kunzisoft.keepass.tests.crypto; - -import com.kunzisoft.keepass.crypto.CipherFactory; - -import junit.framework.TestCase; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Random; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -import static org.junit.Assert.assertArrayEquals; - -public class AESTest extends TestCase { - - 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); - - // Test random larger sizes - int size = mRand.nextInt(494) + 18; - testFinal(size); - } - - private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { - - // 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 IV - byte[] ivArray = new byte[16]; - mRand.nextBytes(ivArray); - IvParameterSpec iv = new IvParameterSpec(ivArray); - - Cipher android = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding", true); - android.init(Cipher.ENCRYPT_MODE, key, iv); - byte[] outAndroid = android.doFinal(input, 0, dataSize); - - Cipher nat = CipherFactory.INSTANCE.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); - } - - -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/AESTest.kt b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/AESTest.kt new file mode 100644 index 000000000..590914330 --- /dev/null +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/AESTest.kt @@ -0,0 +1,85 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.tests.crypto + +import com.kunzisoft.keepass.crypto.CipherFactory + +import junit.framework.TestCase + +import java.security.InvalidAlgorithmParameterException +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import java.util.Random + +import javax.crypto.BadPaddingException +import javax.crypto.Cipher +import javax.crypto.IllegalBlockSizeException +import javax.crypto.NoSuchPaddingException +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +import org.junit.Assert.assertArrayEquals + +class AESTest : TestCase() { + + private val mRand = Random() + + @Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidAlgorithmParameterException::class) + fun testEncrypt() { + // Test above below and at the blocksize + testFinal(15) + testFinal(16) + testFinal(17) + + // Test random larger sizes + val size = mRand.nextInt(494) + 18 + testFinal(size) + } + + @Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class) + private fun testFinal(dataSize: Int) { + + // Generate some input + val input = ByteArray(dataSize) + mRand.nextBytes(input) + + // Generate key + val keyArray = ByteArray(32) + mRand.nextBytes(keyArray) + val key = SecretKeySpec(keyArray, "AES") + + // Generate IV + val ivArray = ByteArray(16) + mRand.nextBytes(ivArray) + val iv = IvParameterSpec(ivArray) + + val android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true) + android.init(Cipher.ENCRYPT_MODE, key, iv) + val outAndroid = android.doFinal(input, 0, dataSize) + + val nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding") + nat.init(Cipher.ENCRYPT_MODE, key, iv) + val outNative = nat.doFinal(input, 0, dataSize) + + assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative) + } + + +} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/CipherTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/CipherTest.java deleted file mode 100644 index ce2906456..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/CipherTest.java +++ /dev/null @@ -1,100 +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 . - * - */ -package com.kunzisoft.keepass.tests.crypto; - -import static org.junit.Assert.assertArrayEquals; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Random; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.CipherOutputStream; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; - -import junit.framework.TestCase; - -import com.kunzisoft.keepass.crypto.CipherFactory; -import com.kunzisoft.keepass.crypto.engine.AesEngine; -import com.kunzisoft.keepass.crypto.engine.CipherEngine; -import com.kunzisoft.keepass.stream.BetterCipherInputStream; -import com.kunzisoft.keepass.stream.LEDataInputStream; - -public class CipherTest extends TestCase { - 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]; - - byte[] plaintext = new byte[1024]; - - rand.nextBytes(key); - rand.nextBytes(iv); - rand.nextBytes(plaintext); - - CipherEngine aes = CipherFactory.INSTANCE.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); - - assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext); - } - - 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[] plaintext = new byte[MESSAGE_LENGTH]; - - rand.nextBytes(key); - rand.nextBytes(iv); - rand.nextBytes(plaintext); - - CipherEngine aes = CipherFactory.INSTANCE.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(); - - byte[] secrettext = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(secrettext); - BetterCipherInputStream cis = new BetterCipherInputStream(bis, decrypt); - LEDataInputStream lis = new LEDataInputStream(cis); - - byte[] decrypttext = lis.readBytes(MESSAGE_LENGTH); - - assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext); - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/CipherTest.kt b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/CipherTest.kt new file mode 100644 index 000000000..0d19ba794 --- /dev/null +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/CipherTest.kt @@ -0,0 +1,102 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.tests.crypto + +import org.junit.Assert.assertArrayEquals + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.security.InvalidAlgorithmParameterException +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import java.util.Random + +import javax.crypto.BadPaddingException +import javax.crypto.Cipher +import javax.crypto.CipherOutputStream +import javax.crypto.IllegalBlockSizeException +import javax.crypto.NoSuchPaddingException + +import junit.framework.TestCase + +import com.kunzisoft.keepass.crypto.CipherFactory +import com.kunzisoft.keepass.crypto.engine.AesEngine +import com.kunzisoft.keepass.crypto.engine.CipherEngine +import com.kunzisoft.keepass.stream.BetterCipherInputStream +import com.kunzisoft.keepass.stream.LEDataInputStream + +class CipherTest : TestCase() { + private val rand = Random() + + @Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class) + fun testCipherFactory() { + val key = ByteArray(32) + val iv = ByteArray(16) + + val plaintext = ByteArray(1024) + + rand.nextBytes(key) + rand.nextBytes(iv) + rand.nextBytes(plaintext) + + val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID) + val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv) + val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv) + + val secrettext = encrypt.doFinal(plaintext) + val decrypttext = decrypt.doFinal(secrettext) + + assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext) + } + + @Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class, IOException::class) + fun testCipherStreams() { + val MESSAGE_LENGTH = 1024 + + val key = ByteArray(32) + val iv = ByteArray(16) + + val plaintext = ByteArray(MESSAGE_LENGTH) + + rand.nextBytes(key) + rand.nextBytes(iv) + rand.nextBytes(plaintext) + + val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID) + val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv) + val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv) + + val bos = ByteArrayOutputStream() + val cos = CipherOutputStream(bos, encrypt) + cos.write(plaintext) + cos.close() + + val secrettext = bos.toByteArray() + + val bis = ByteArrayInputStream(secrettext) + val cis = BetterCipherInputStream(bis, decrypt) + val lis = LEDataInputStream(cis) + + val decrypttext = lis.readBytes(MESSAGE_LENGTH) + + assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext) + } +} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/FinalKeyTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/FinalKeyTest.java deleted file mode 100644 index 2c36c9bac..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/FinalKeyTest.java +++ /dev/null @@ -1,66 +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 . - * - */ -package com.kunzisoft.keepass.tests.crypto; - -import static org.junit.Assert.assertArrayEquals; - -import java.io.IOException; -import java.util.Random; - -import junit.framework.TestCase; - -import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey; -import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey; - -public class FinalKeyTest extends TestCase { - private Random mRand; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - 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); - } - - 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); - - AndroidFinalKey aKey = new AndroidFinalKey(); - androidKey = aKey.transformMasterKey(seed, key, rounds); - - NativeFinalKey nKey = new NativeFinalKey(); - nativeKey = nKey.transformMasterKey(seed, key, rounds); - - assertArrayEquals("Does not match", androidKey, nativeKey); - - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/FinalKeyTest.kt b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/FinalKeyTest.kt new file mode 100644 index 000000000..ce98f6db9 --- /dev/null +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/FinalKeyTest.kt @@ -0,0 +1,68 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.tests.crypto + +import org.junit.Assert.assertArrayEquals + +import java.io.IOException +import java.util.Random + +import junit.framework.TestCase + +import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey +import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey + +class FinalKeyTest : TestCase() { + private var mRand: Random? = null + + @Throws(Exception::class) + override fun setUp() { + super.setUp() + + mRand = Random() + } + + @Throws(IOException::class) + fun testNativeAndroid() { + // Test both an old and an even number to test my flip variable + testNativeFinalKey(5) + testNativeFinalKey(6) + } + + @Throws(IOException::class) + private fun testNativeFinalKey(rounds: Int) { + val seed = ByteArray(32) + val key = ByteArray(32) + val nativeKey: ByteArray + val androidKey: ByteArray + + mRand!!.nextBytes(seed) + mRand!!.nextBytes(key) + + val aKey = AndroidFinalKey() + androidKey = aKey.transformMasterKey(seed, key, rounds.toLong()) + + val nKey = NativeFinalKey() + nativeKey = nKey.transformMasterKey(seed, key, rounds.toLong()) + + assertArrayEquals("Does not match", androidKey, nativeKey) + + } +} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/DeleteEntry.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/DeleteEntry.java deleted file mode 100644 index ed01c2334..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/DeleteEntry.java +++ /dev/null @@ -1,111 +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 . - * - */ -package com.kunzisoft.keepass.tests.database; - -import android.test.AndroidTestCase; -import com.kunzisoft.keepass.database.element.GroupVersioned; -import com.kunzisoft.keepass.database.element.PwDatabase; -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"; - - public void testDelete() { - - /* - Database db; - - Context ctx = getContext(); - - try { - db = TestData.GetDb(ctx, ASSET, PASSWORD, KEYFILE, FILENAME); - } catch (Exception e) { - assertTrue("Failed to open database: " + e.getMessage(), false); - return; - } - - PwDatabaseV3 pm = (PwDatabaseV3) db.getPwDatabase(); - GroupVersioned group1 = getGroup(pm, GROUP1_NAME); - assertNotNull("Could not find group1", group1); - - // Delete the group - DeleteGroupRunnable task = new DeleteGroupRunnable(null, db, group1, null, true); - task.run(); - - // Verify the entries were deleted - PwEntryInterface entry1 = getEntry(pm, ENTRY1_NAME); - assertNull("Entry 1 was not removed", entry1); - - PwEntryInterface entry2 = getEntry(pm, ENTRY2_NAME); - assertNull("Entry 2 was not removed", entry2); - - // Verify the entries were removed from the search index - SearchDbHelper dbHelp = new SearchDbHelper(ctx); - GroupVersioned results1 = dbHelp.search(db.getPwDatabase(), ENTRY1_NAME, 100); - GroupVersioned results2 = dbHelp.search(db.getPwDatabase(), ENTRY2_NAME, 100); - - assertEquals("Entry1 was not removed from the search results", 0, results1.numbersOfChildEntries()); - assertEquals("Entry2 was not removed from the search results", 0, results2.numbersOfChildEntries()); - - // Verify the group was deleted - group1 = getGroup(pm, GROUP1_NAME); - assertNull("Group 1 was not removed.", group1); - */ - - } - - private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) { - /* - TODO test - List entries = pm.getEntries(); - for ( int i = 0; i < entries.size(); i++ ) { - PwEntryV3 entry = entries.get(i); - if ( entry.getTitle().equals(name) ) { - return entry; - } - } - */ - return null; - - } - - private GroupVersioned getGroup(PwDatabase pm, String name) { - /* - List groups = pm.getGroups(); - for ( int i = 0; i < groups.size(); i++ ) { - GroupVersioned group = groups.get(i); - if ( group.getTitle().equals(name) ) { - return group; - } - } - */ - - return null; - } - - -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/EntryV4.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/EntryV4.java deleted file mode 100644 index fa009c76f..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/EntryV4.java +++ /dev/null @@ -1,56 +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 . - * - */ -package com.kunzisoft.keepass.tests.database; - -import com.kunzisoft.keepass.database.element.PwDatabaseV4; -import com.kunzisoft.keepass.database.element.PwEntryV4; - -import junit.framework.TestCase; - -public class EntryV4 extends TestCase { - - public void testBackup() { - /* - PwDatabaseV4 db = new PwDatabaseV4(); - - db.setHistoryMaxItems(2); - - PwEntryV4 entry = new PwEntryV4(); - entry.startToManageFieldReferences(db); - entry.setTitle("Title1"); - entry.setUsername("User1"); - entry.createBackup(db); - - entry.setTitle("Title2"); - entry.setUsername("User2"); - entry.createBackup(db); - - entry.setTitle("Title3"); - entry.setUsername("User3"); - entry.createBackup(db); - - PwEntryV4 backup = entry.getHistory().get(0); - entry.stopToManageFieldReferences(); - assertEquals("Title2", backup.getTitle()); - assertEquals("User2", backup.getUsername()); - */ - } - -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb3.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb3.java deleted file mode 100644 index 46121d9db..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb3.java +++ /dev/null @@ -1,53 +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 . - * - */ -package com.kunzisoft.keepass.tests.database; - -import android.test.AndroidTestCase; - -public class Kdb3 extends AndroidTestCase { - - private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception { - /* - Context ctx = getContext(); - - File sdcard = Environment.getExternalStorageDirectory(); - String keyPath = sdcard.getAbsolutePath() + "/key"; - - TestUtil.extractKey(ctx, keyAsset, keyPath); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open(dbAsset, AssetManager.ACCESS_STREAMING); - - ImporterV3 importer = new ImporterV3(); - importer.openDatabase(is, password, TestUtil.getKeyFileInputStream(ctx, keyPath)); - - is.close(); - */ - } - - 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"); - } - -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb3Twofish.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb3Twofish.java deleted file mode 100644 index 6c1105909..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb3Twofish.java +++ /dev/null @@ -1,41 +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 . - * - */ -package com.kunzisoft.keepass.tests.database; - -import android.test.AndroidTestCase; - -public class Kdb3Twofish extends AndroidTestCase { - public void testReadTwofish() throws Exception { - /* - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("twofish.kdb", AssetManager.ACCESS_STREAMING); - - ImporterV3 importer = new ImporterV3(); - - PwDatabaseV3 db = importer.openDatabase(is, "12345", null); - - assertTrue(db.getEncryptionAlgorithm() == PwEncryptionAlgorithm.Twofish); - - is.close(); - */ - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb4.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb4.java deleted file mode 100644 index 74e6323e2..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb4.java +++ /dev/null @@ -1,171 +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 . - * - */ -package com.kunzisoft.keepass.tests.database; - -import android.content.Context; -import android.content.res.AssetManager; -import android.test.AndroidTestCase; - -import com.kunzisoft.keepass.database.exception.InvalidDBException; -import com.kunzisoft.keepass.database.exception.PwDbOutputException; -import com.kunzisoft.keepass.tests.TestUtil; - -import java.io.IOException; -import java.io.InputStream; - -public class Kdb4 extends AndroidTestCase { - - public void testDetection() throws IOException, InvalidDBException { - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); - - /* - TODO Test - Importer importer = ImporterFactory.createImporter(is); - - assertTrue(importer instanceof ImporterV4); - is.close(); - */ - } - - public void testParsing() throws IOException, InvalidDBException { - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); - - /* - TODO Test - ImporterV4 importer = new ImporterV4(); - importer.openDatabase(is, "12345", null); - - is.close(); - */ - } - - public void testSavingKDBXV3() throws IOException, InvalidDBException, PwDbOutputException { - testSaving("test.kdbx", "12345", "test-out.kdbx"); - } - - public void testSavingKDBXV4() throws IOException, InvalidDBException, PwDbOutputException { - testSaving("test-kdbxv4.kdbx", "1", "test-kdbxv4-out.kdbx"); - } - - private void testSaving(String inputFile, String password, String outputFile) throws IOException, InvalidDBException, PwDbOutputException { - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open(inputFile, AssetManager.ACCESS_STREAMING); - - /* - TODO Test - ImporterV4 importer = new ImporterV4(); - PwDatabaseV4 db = importer.openDatabase(is, password, null); - is.close(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - PwDbV4Output output = (PwDbV4Output) PwDbOutput.getInstance(db, bos); - output.output(); - - byte[] data = bos.toByteArray(); - - FileOutputStream fos = new FileOutputStream(TestUtil.getSdPath(outputFile), false); - - InputStream bis = new ByteArrayInputStream(data); - bis = new CopyInputStream(bis, fos); - importer = new ImporterV4(); - db = importer.openDatabase(bis, password, null); - bis.close(); - - fos.close(); - */ - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - - TestUtil.extractKey(getContext(), "keyfile.key", TestUtil.getSdPath("key")); - TestUtil.extractKey(getContext(), "binary.key", TestUtil.getSdPath("key-binary")); - } - - public void testComposite() throws IOException, InvalidDBException { - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("keyfile.kdbx", AssetManager.ACCESS_STREAMING); - - /* - TODO Test - ImporterV4 importer = new ImporterV4(); - importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key"))); - - is.close(); - */ - - } - - public void testCompositeBinary() throws IOException, InvalidDBException { - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("keyfile-binary.kdbx", AssetManager.ACCESS_STREAMING); - - /* - TODO Test - ImporterV4 importer = new ImporterV4(); - importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx,TestUtil.getSdPath("key-binary"))); - - is.close(); - */ - } - - public void testKeyfile() throws IOException, InvalidDBException { - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("key-only.kdbx", AssetManager.ACCESS_STREAMING); - /* - TODO Test - ImporterV4 importer = new ImporterV4(); - importer.openDatabase(is, "", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key"))); - - is.close(); - */ - } - - public void testNoGzip() throws IOException, InvalidDBException { - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("no-encrypt.kdbx", AssetManager.ACCESS_STREAMING); - /* - TODO Test - ImporterV4 importer = new ImporterV4(); - importer.openDatabase(is, "12345", null); - - is.close(); - */ - } - -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb4Header.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb4Header.java deleted file mode 100644 index 99ca1a9d1..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb4Header.java +++ /dev/null @@ -1,49 +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 . - * - */ -package com.kunzisoft.keepass.tests.database; - -import android.content.Context; -import android.content.res.AssetManager; -import android.test.AndroidTestCase; - -import java.io.InputStream; - -public class Kdb4Header extends AndroidTestCase { - public void testReadHeader() throws Exception { - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); - - /* - TODO Test - ImporterV4 importer = new ImporterV4(); - - PwDatabaseV4 db = importer.openDatabase(is, "12345", null); - - assertEquals(6000, db.getNumberKeyEncryptionRounds()); - - assertTrue(db.getDataCipher().equals(AesEngine.CIPHER_UUID)); - - is.close(); - */ - - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/SprEngineTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/SprEngineTest.java deleted file mode 100644 index bfa06f696..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/SprEngineTest.java +++ /dev/null @@ -1,70 +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 . - * - */ -package com.kunzisoft.keepass.tests.database; - -import android.content.Context; -import android.content.res.AssetManager; -import android.test.AndroidTestCase; - -import com.kunzisoft.keepass.database.element.PwDatabaseV4; -import com.kunzisoft.keepass.database.element.SprEngineV4; - -import java.io.InputStream; - -public class SprEngineTest extends AndroidTestCase { - private PwDatabaseV4 db; - private SprEngineV4 spr; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); - - /* - TODO Test - ImporterV4 importer = new ImporterV4(); - db = importer.openDatabase(is, "12345", null); - - is.close(); - - 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() { - /* - TODO TEST - UUID entryUUID = decodeUUID(ENCODE_UUID); - - PwEntryV4 entry = (PwEntryV4) db.getEntryById(entryUUID); - - - assertEquals(RESULT, spr.compile(REF, entry, db)); - */ - - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/TestData.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/TestData.java deleted file mode 100644 index 0d4dee7e6..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/TestData.java +++ /dev/null @@ -1,71 +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 . - * - */ -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 Database mDb1; - - /* - - public static Database GetDb1(Context ctx) throws Exception { - return GetDb1(ctx, false); - } - - public static Database GetDb1(Context ctx, boolean forceReload) throws Exception { - if ( mDb1 == null || forceReload ) { - mDb1 = GetDb(ctx, TEST1_KDB, TEST1_PASSWORD, TEST1_KEYFILE, "/sdcard/test1.kdb"); - } - - return mDb1; - } - - public static Database GetDb(Context ctx, String asset, String password, String keyfile, String filename) throws Exception { - AssetManager am = ctx.getAssets(); - InputStream is = am.open(asset, AssetManager.ACCESS_STREAMING); - - Database Db = new Database(); - - InputStream keyIs = TestUtil.getKeyFileInputStream(ctx, keyfile); - - Db.loadData(ctx, is, password, keyIs, Importer.DEBUG); - Uri.Builder b = new Uri.Builder(); - - Db.setUri(b.scheme("file").path(filename).build()); - - return Db; - - } - - public static PwDatabaseV3Debug GetTest1(Context ctx) throws Exception { - if ( mDb1 == null ) { - GetDb1(ctx); - } - - //return (PwDatabaseV3Debug) mDb1.getPwDatabase(); - return null; - } - */ -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/output/PwManagerOutputTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/output/PwManagerOutputTest.java deleted file mode 100644 index aa77c7843..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/output/PwManagerOutputTest.java +++ /dev/null @@ -1,126 +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 . -* -*/ -package com.kunzisoft.keepass.tests.output; - -import android.test.AndroidTestCase; - -public class PwManagerOutputTest extends AndroidTestCase { - // PwDatabaseV3Debug mPM; - - /* - @Override - protected void setUp() throws Exception { - super.setUp(); - - mPM = TestData.GetTest1(getContext()); - } - - public void testPlainContent() throws IOException, PwDbOutputException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - PwDbV3Output pos = new PwDbV3OutputDebug(mPM, bos, true); - pos.outputPlanGroupAndEntries(bos); - - assertTrue("No output", bos.toByteArray().length > 0); - assertArrayEquals("Group and entry output doesn't match.", mPM.getPostHeader(), bos.toByteArray()); - - } - - public void testChecksum() throws NoSuchAlgorithmException, IOException, PwDbOutputException { - //FileOutputStream fos = new FileOutputStream("/dev/null"); - NullOutputStream nos = new NullOutputStream(); - MessageDigest md = MessageDigest.getInstance("SHA-256"); - - DigestOutputStream dos = new DigestOutputStream(nos, md); - - PwDbV3Output pos = new PwDbV3OutputDebug(mPM, dos, true); - pos.outputPlanGroupAndEntries(dos); - dos.close(); - - byte[] digest = md.digest(); - assertTrue("No output", digest.length > 0); - assertArrayEquals("Hash of groups and entries failed.", mPM.getDbHeader().contentsHash, digest); - } - - private void assertHeadersEquals(PwDbHeaderV3 expected, PwDbHeaderV3 actual) { - assertEquals("Flags unequal", expected.flags, actual.flags); - assertEquals("Entries unequal", expected.numEntries, actual.numEntries); - assertEquals("Groups unequal", expected.numGroups, actual.numGroups); - assertEquals("Key Rounds unequal", expected.numKeyEncRounds, actual.numKeyEncRounds); - assertEquals("Signature1 unequal", expected.signature1, actual.signature1); - assertEquals("Signature2 unequal", expected.signature2, actual.signature2); - assertTrue("Version incompatible", PwDbHeaderV3.compatibleHeaders(expected.version, actual.version)); - assertArrayEquals("Hash unequal", expected.contentsHash, actual.contentsHash); - assertArrayEquals("IV unequal", expected.encryptionIV, actual.encryptionIV); - assertArrayEquals("Seed unequal", expected.masterSeed, actual.masterSeed); - assertArrayEquals("Seed2 unequal", expected.transformSeed, actual.transformSeed); - } - - public void testHeader() throws PwDbOutputException, IOException { - ByteArrayOutputStream bActual = new ByteArrayOutputStream(); - PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true); - PwDbHeaderV3 header = pActual.outputHeader(bActual); - - ByteArrayOutputStream bExpected = new ByteArrayOutputStream(); - PwDbHeaderOutputV3 outExpected = new PwDbHeaderOutputV3(mPM.getDbHeader(), bExpected); - outExpected.output(); - - assertHeadersEquals(mPM.getDbHeader(), header); - assertTrue("No output", bActual.toByteArray().length > 0); - assertArrayEquals("Header does not match.", bExpected.toByteArray(), bActual.toByteArray()); - } - - public void testFinalKey() throws PwDbOutputException { - ByteArrayOutputStream bActual = new ByteArrayOutputStream(); - PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true); - PwDbHeader hActual = pActual.outputHeader(bActual); - byte[] finalKey = pActual.getFinalKey(hActual); - - assertArrayEquals("Keys mismatched", mPM.getFinalKey(), finalKey); - - } - - public void testFullWrite() throws IOException, PwDbOutputException { - AssetManager am = getContext().getAssets(); - InputStream is = am.open("test1.kdb"); - - // Pull file into byte array (for streaming fun) - ByteArrayOutputStream bExpected = new ByteArrayOutputStream(); - while (true) { - int data = is.read(); - if ( data == -1 ) { - break; - } - bExpected.write(data); - } - - ByteArrayOutputStream bActual = new ByteArrayOutputStream(); - PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true); - pActual.output(); - //pActual.close(); - - FileOutputStream fos = new FileOutputStream(TestUtil.getSdPath("test1_out.kdb")); - fos.write(bActual.toByteArray()); - fos.close(); - assertArrayEquals("Databases do not match.", bExpected.toByteArray(), bActual.toByteArray()); - - } - */ -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/search/SearchTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/search/SearchTest.java deleted file mode 100644 index 68b221322..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/search/SearchTest.java +++ /dev/null @@ -1,70 +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 . - * - */ -package com.kunzisoft.keepass.tests.search; - - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.test.AndroidTestCase; -import com.kunzisoft.keepass.database.element.Database; -import com.kunzisoft.keepass.database.element.GroupVersioned; - -public class SearchTest extends AndroidTestCase { - - private Database mDb; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - //mDb = TestData.GetDb1(getContext(), true); - } - - 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"); - - //assertTrue("Search result not found.", results.numbersOfChildEntries() > 0); - } - - public void testBackupExcluded() { - updateOmitSetting(true); - GroupVersioned results = mDb.search("BackupOnly"); - - //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(); - - editor.putBoolean("settings_omitbackup_key", setting); - editor.commit(); - - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/HashedBlock.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/HashedBlock.java deleted file mode 100644 index ed14b8552..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/HashedBlock.java +++ /dev/null @@ -1,110 +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 . - * - */ -package com.kunzisoft.keepass.tests.stream; - -import static org.junit.Assert.assertArrayEquals; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Random; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -import junit.framework.TestCase; - -import com.kunzisoft.keepass.stream.HashedBlockInputStream; -import com.kunzisoft.keepass.stream.HashedBlockOutputStream; - -public class HashedBlock extends TestCase { - - private static Random rand = new Random(); - - public void testBlockAligned() throws IOException { - testSize(1024, 1024); - } - - public void testOffset() throws IOException { - testSize(1500, 1024); - } - - private void testSize(int blockSize, int bufferSize) throws IOException { - byte[] orig = new byte[blockSize]; - - rand.nextBytes(orig); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - HashedBlockOutputStream output = new HashedBlockOutputStream(bos, bufferSize); - output.write(orig); - output.close(); - - byte[] encoded = bos.toByteArray(); - - 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; - } - - decoded.write(buf, 0, read); - } - - byte[] out = decoded.toByteArray(); - - assertArrayEquals(orig, out); - - } - - public void testGZIPStream() throws IOException { - final int testLength = 32000; - - byte[] orig = new byte[testLength]; - rand.nextBytes(orig); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - HashedBlockOutputStream hos = new HashedBlockOutputStream(bos); - GZIPOutputStream zos = new GZIPOutputStream(hos); - - 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[] uncompressed = new byte[testLength]; - - int read = 0; - while (read != -1 && testLength - read > 0) { - read += zis.read(uncompressed, read, testLength - read); - - } - - assertArrayEquals("Output not equal to input", orig, uncompressed); - - - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/HashedBlock.kt b/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/HashedBlock.kt new file mode 100644 index 000000000..4e80c5226 --- /dev/null +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/HashedBlock.kt @@ -0,0 +1,117 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.tests.stream + +import org.junit.Assert.assertArrayEquals + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.Random +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +import junit.framework.TestCase + +import com.kunzisoft.keepass.stream.HashedBlockInputStream +import com.kunzisoft.keepass.stream.HashedBlockOutputStream + +class HashedBlock : TestCase() { + + @Throws(IOException::class) + fun testBlockAligned() { + testSize(1024, 1024) + } + + @Throws(IOException::class) + fun testOffset() { + testSize(1500, 1024) + } + + @Throws(IOException::class) + private fun testSize(blockSize: Int, bufferSize: Int) { + val orig = ByteArray(blockSize) + + rand.nextBytes(orig) + + val bos = ByteArrayOutputStream() + val output = HashedBlockOutputStream(bos, bufferSize) + output.write(orig) + output.close() + + val encoded = bos.toByteArray() + + val bis = ByteArrayInputStream(encoded) + val input = HashedBlockInputStream(bis) + + val decoded = ByteArrayOutputStream() + while (true) { + val buf = ByteArray(1024) + val read = input.read(buf) + if (read == -1) { + break + } + + decoded.write(buf, 0, read) + } + + val out = decoded.toByteArray() + + assertArrayEquals(orig, out) + + } + + @Throws(IOException::class) + fun testGZIPStream() { + val testLength = 32000 + + val orig = ByteArray(testLength) + rand.nextBytes(orig) + + val bos = ByteArrayOutputStream() + val hos = HashedBlockOutputStream(bos) + val zos = GZIPOutputStream(hos) + + zos.write(orig) + zos.close() + + val compressed = bos.toByteArray() + val bis = ByteArrayInputStream(compressed) + val his = HashedBlockInputStream(bis) + val zis = GZIPInputStream(his) + + val uncompressed = ByteArray(testLength) + + var read = 0 + while (read != -1 && testLength - read > 0) { + read += zis.read(uncompressed, read, testLength - read) + + } + + assertArrayEquals("Output not equal to input", orig, uncompressed) + + + } + + companion object { + + private val rand = Random() + } +} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/utils/StringUtilTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/utils/StringUtilTest.java deleted file mode 100644 index 12151eadd..000000000 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/utils/StringUtilTest.java +++ /dev/null @@ -1,57 +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 . - * - */ -package com.kunzisoft.keepass.tests.utils; - -import java.util.Locale; - -import com.kunzisoft.keepass.utils.StringUtil; - -import junit.framework.TestCase; - -public class StringUtilTest extends TestCase { - private final String text = "AbCdEfGhIj"; - private final String search = "BcDe"; - private final String badSearch = "Ed"; - - 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 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)); - } - - public void testReplaceAllIgnoresCase2() { - assertEquals(repText, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH)); - } -} diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/utils/StringUtilTest.kt b/app/src/androidTest/java/com/kunzisoft/keepass/tests/utils/StringUtilTest.kt new file mode 100644 index 000000000..d681c63ed --- /dev/null +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/utils/StringUtilTest.kt @@ -0,0 +1,58 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.tests.utils + +import java.util.Locale + +import com.kunzisoft.keepass.utils.StringUtil + +import junit.framework.TestCase + +class StringUtilTest : TestCase() { + private val text = "AbCdEfGhIj" + private val search = "BcDe" + private val badSearch = "Ed" + + private val repText = "AbCtestingaBc" + private val repSearch = "ABc" + private val repSearchBad = "CCCCCC" + private val repNew = "12345" + private val repResult = "12345testing12345" + + fun testIndexOfIgnoreCase1() { + assertEquals(1, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH)) + } + + fun testIndexOfIgnoreCase2() { + assertEquals(-1f, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH).toFloat(), 2f) + } + + fun testIndexOfIgnoreCase3() { + assertEquals(-1, StringUtil.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH)) + } + + fun testReplaceAllIgnoresCase1() { + assertEquals(repResult, StringUtil.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH)) + } + + fun testReplaceAllIgnoresCase2() { + assertEquals(repText, StringUtil.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH)) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0f48dc49b..b19a964e5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + + @@ -48,7 +48,7 @@ - + @@ -71,29 +71,28 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + @@ -129,6 +128,7 @@ + diff --git a/app/src/main/assets/accent.kdb b/app/src/main/assets/accent.kdb deleted file mode 100644 index b1d7eb220..000000000 Binary files a/app/src/main/assets/accent.kdb and /dev/null differ diff --git a/app/src/main/assets/binary-key.kdb b/app/src/main/assets/binary-key.kdb deleted file mode 100644 index 70449ba3b..000000000 Binary files a/app/src/main/assets/binary-key.kdb and /dev/null differ diff --git a/app/src/main/assets/binary.key b/app/src/main/assets/binary.key deleted file mode 100644 index fd3b8274d..000000000 --- a/app/src/main/assets/binary.key +++ /dev/null @@ -1,2 +0,0 @@ -v7gx"Dm]tIWRPgy/˰1X fW[F%\up4 --t;z \ No newline at end of file diff --git a/app/src/main/assets/delete.kdb b/app/src/main/assets/delete.kdb deleted file mode 100644 index 876547bc5..000000000 Binary files a/app/src/main/assets/delete.kdb and /dev/null differ diff --git a/app/src/main/assets/kdb_with_xml_keyfile.kdb b/app/src/main/assets/kdb_with_xml_keyfile.kdb deleted file mode 100644 index 87cc24e5b..000000000 Binary files a/app/src/main/assets/kdb_with_xml_keyfile.kdb and /dev/null differ diff --git a/app/src/main/assets/key-only.kdbx b/app/src/main/assets/key-only.kdbx deleted file mode 100644 index d65546696..000000000 Binary files a/app/src/main/assets/key-only.kdbx and /dev/null differ diff --git a/app/src/main/assets/keyfile-binary.kdbx b/app/src/main/assets/keyfile-binary.kdbx deleted file mode 100644 index 0bd15e95b..000000000 Binary files a/app/src/main/assets/keyfile-binary.kdbx and /dev/null differ diff --git a/app/src/main/assets/keyfile.kdbx b/app/src/main/assets/keyfile.kdbx deleted file mode 100644 index da16d8e44..000000000 Binary files a/app/src/main/assets/keyfile.kdbx and /dev/null differ diff --git a/app/src/main/assets/keyfile.key b/app/src/main/assets/keyfile.key deleted file mode 100644 index 9b07baffd..000000000 --- a/app/src/main/assets/keyfile.key +++ /dev/null @@ -1,9 +0,0 @@ - - - - 1.00 - - - zaTWphVNtRbspnwkqjy8FGTy5IqCUx9+FNb5H+VdB24= - - diff --git a/app/src/main/assets/no-encrypt.kdbx b/app/src/main/assets/no-encrypt.kdbx deleted file mode 100644 index 88b341b83..000000000 Binary files a/app/src/main/assets/no-encrypt.kdbx and /dev/null differ diff --git a/app/src/main/assets/test-kdbxv4.kdbx b/app/src/main/assets/test-kdbxv4.kdbx deleted file mode 100644 index 092d9beeb..000000000 Binary files a/app/src/main/assets/test-kdbxv4.kdbx and /dev/null differ diff --git a/app/src/main/assets/test.kdbx b/app/src/main/assets/test.kdbx deleted file mode 100644 index 0ac33d1c5..000000000 Binary files a/app/src/main/assets/test.kdbx and /dev/null differ diff --git a/app/src/main/assets/test1.kdb b/app/src/main/assets/test1.kdb deleted file mode 100644 index 390ca3255..000000000 Binary files a/app/src/main/assets/test1.kdb and /dev/null differ diff --git a/app/src/main/assets/twofish.kdb b/app/src/main/assets/twofish.kdb deleted file mode 100644 index fb08f0466..000000000 Binary files a/app/src/main/assets/twofish.kdb and /dev/null differ diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt index c4fb057c2..8ca652327 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt @@ -21,7 +21,7 @@ package com.kunzisoft.keepass.activities import android.content.pm.PackageManager.NameNotFoundException import android.os.Bundle -import android.support.v7.widget.Toolbar +import androidx.appcompat.widget.Toolbar import android.util.Log import android.view.MenuItem import android.widget.TextView diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index ec226e9a5..6a54c6b1d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -19,15 +19,13 @@ 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.design.widget.CollapsingToolbarLayout -import android.support.v7.app.AlertDialog -import android.support.v7.widget.Toolbar +import com.google.android.material.appbar.CollapsingToolbarLayout +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar import android.util.Log import android.view.Menu import android.view.MenuItem @@ -45,12 +43,11 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService 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 +import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.EntryContentsView class EntryActivity : LockingHideActivity() { @@ -79,7 +76,7 @@ class EntryActivity : LockingHideActivity() { supportActionBar?.setDisplayShowHomeEnabled(true) val currentDatabase = Database.getInstance() - readOnly = currentDatabase.isReadOnly || readOnly + mReadOnly = currentDatabase.isReadOnly || mReadOnly mShowPassword = !PreferencesUtil.isPasswordMask(this) @@ -101,8 +98,8 @@ class EntryActivity : LockingHideActivity() { 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) + val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) + iconColor = taIconColor.getColor(0, Color.BLACK) taIconColor.recycle() // Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set @@ -159,65 +156,86 @@ class EntryActivity : LockingHideActivity() { // Assign basic fields entryContentsView?.assignUserName(entry.username) entryContentsView?.assignUserNameCopyListener(View.OnClickListener { + database.startManageEntry(entry) clipboardHelper?.timeoutCopyToClipboard(entry.username, getString(R.string.copy_field, getString(R.string.entry_user_name))) + database.stopManageEntry(entry) }) - val allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this) - entryContentsView?.assignPassword(entry.password, allowCopyPassword) - if (allowCopyPassword) { + val isFirstTimeAskAllowCopyPasswordAndProtectedFields = + PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this) + val allowCopyPasswordAndProtectedFields = + PreferencesUtil.allowCopyPasswordAndProtectedFields(this) + + val showWarningClipboardDialogOnClickListener = View.OnClickListener { + AlertDialog.Builder(this@EntryActivity) + .setMessage(getString(R.string.allow_copy_password_warning) + + "\n\n" + + getString(R.string.clipboard_warning)) + .create().apply { + setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) {dialog, _ -> + PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true) + dialog.dismiss() + fillEntryDataInContentsView(entry) + } + setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ -> + PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false) + dialog.dismiss() + fillEntryDataInContentsView(entry) + } + show() + } + } + + entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields) + if (allowCopyPasswordAndProtectedFields) { entryContentsView?.assignPasswordCopyListener(View.OnClickListener { + database.startManageEntry(entry) clipboardHelper?.timeoutCopyToClipboard(entry.password, getString(R.string.copy_field, getString(R.string.entry_password))) + database.stopManageEntry(entry) }) } 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() - }) + if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) { + entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener) } else { entryContentsView?.assignPasswordCopyListener(null) } } entryContentsView?.assignURL(entry.url) - entryContentsView?.setHiddenPasswordStyle(!mShowPassword) entryContentsView?.assignComment(entry.notes) // Assign custom fields - if (entry.allowExtraFields()) { + if (entry.allowCustomFields()) { 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) - ) - }) + for (element in entry.customFields.entries) { + val label = element.key + val value = element.value + + val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields + if (allowCopyProtectedField) { + entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener { + clipboardHelper?.timeoutCopyToClipboard( + value.toString(), + getString(R.string.copy_field, label) + ) + }) + } else { + // If dialog not already shown + if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) { + entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener) + } else { + entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null) + } + } } } + entryContentsView?.setHiddenPasswordStyle(!mShowPassword) // Assign dates entry.creationTime.date?.let { @@ -271,7 +289,7 @@ class EntryActivity : LockingHideActivity() { inflater.inflate(R.menu.entry, menu) inflater.inflate(R.menu.database_lock, menu) - if (readOnly) { + if (mReadOnly) { menu.findItem(R.id.menu_edit)?.isVisible = false } @@ -306,7 +324,7 @@ class EntryActivity : LockingHideActivity() { private fun performedNextEducation(entryActivityEducation: EntryActivityEducation, menu: Menu) { - if (entryContentsView?.isUserNamePresent == true + val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true && entryActivityEducation.checkAndPerformedEntryCopyEducation( findViewById(R.id.entry_user_name_action_image), { @@ -317,23 +335,29 @@ class EntryActivity : LockingHideActivity() { { // Launch autofill settings startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java)) - })) - else if (toolbar?.findViewById(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)))) - })) - ; + }) + + if (!entryCopyEducationPerformed) { + // entryEditEducationPerformed + toolbar?.findViewById(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, + UriUtil.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_contribute -> { + MenuUtil.onContributionItemSelected(this) + return true + } R.id.menu_toggle_pass -> { mShowPassword = !mShowPassword @@ -357,11 +381,7 @@ class EntryActivity : LockingHideActivity() { url = "http://$url" } - try { - Util.gotoUrl(this, url) - } catch (e: ActivityNotFoundException) { - Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show() - } + UriUtil.gotoUrl(this, url) return true } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index 5e6dbe8bc..234cef43b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -22,7 +22,7 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import android.os.Handler -import android.support.v7.widget.Toolbar +import androidx.appcompat.widget.Toolbar import android.util.Log import android.view.Menu import android.view.MenuItem @@ -45,7 +45,9 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.view.EntryEditContentsView -class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener { +class EntryEditActivity : LockingHideActivity(), + IconPickerDialogFragment.IconPickerListener, + GeneratePasswordDialogFragment.GeneratePasswordListener { private var mDatabase: Database? = null @@ -150,37 +152,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi saveView = findViewById(R.id.entry_edit_save) saveView?.setOnClickListener { saveEntry() } - entryEditContentsView?.allowCustomField(mNewEntry?.allowExtraFields() == true) { addNewCustomField() } + entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) { addNewCustomField() } // Verify the education views entryEditActivityEducation = EntryEditActivityEducation(this) - entryEditActivityEducation?.let { - Handler().post { performedNextEducation(it) } - } - } - - private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { - val passwordView = entryEditContentsView?.generatePasswordView - val addNewFieldView = entryEditContentsView?.addNewFieldView - - if (passwordView != null - && entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( - passwordView, - { - openPasswordGenerator() - }, - { - performedNextEducation(entryEditActivityEducation) - } - )) - else if (mNewEntry != null && mNewEntry!!.allowExtraFields() && !mNewEntry!!.containsCustomFields() - && addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE - && entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( - addNewFieldView, - { - addNewCustomField() - })) - ; } private fun populateViewsWithEntry(newEntry: EntryVersioned) { @@ -197,8 +172,8 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi url = newEntry.url password = newEntry.password notes = newEntry.notes - newEntry.fields.doActionToAllCustomProtectedField { key, value -> - addNewCustomField(key, value) + for (entry in newEntry.customFields.entries) { + addNewCustomField(entry.key, entry.value) } } } @@ -281,7 +256,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi newEntry, parent, afterActionNodeFinishRunnable, - !readOnly) + !mReadOnly) } } else { @@ -291,7 +266,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi oldEntry, newEntry, afterActionNodeFinishRunnable, - !readOnly) + !mReadOnly) } } actionRunnable?.let { runnable -> @@ -309,9 +284,39 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi inflater.inflate(R.menu.database_lock, menu) MenuUtil.contributionMenuInflater(inflater, menu) + entryEditActivityEducation?.let { + Handler().post { performedNextEducation(it) } + } + return true } + private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { + val passwordView = entryEditContentsView?.generatePasswordView + val addNewFieldView = entryEditContentsView?.addNewFieldView + + val generatePasswordEducationPerformed = passwordView != null + && entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( + passwordView, + { + openPasswordGenerator() + }, + { + performedNextEducation(entryEditActivityEducation) + } + ) + if (!generatePasswordEducationPerformed) { + // entryNewFieldEducationPerformed + mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty() + && addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE + && entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( + addNewFieldView, + { + addNewCustomField() + }) + } + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_lock -> { @@ -319,7 +324,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi return true } - R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this) + R.id.menu_contribute -> { + MenuUtil.onContributionItemSelected(this) + return true + } android.R.id.home -> finish() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index 8d957b4b8..70ef64046 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -19,7 +19,7 @@ */ package com.kunzisoft.keepass.activities -import android.Manifest +import android.annotation.SuppressLint import android.app.Activity import android.app.assist.AssistStructure import android.content.Intent @@ -29,56 +29,42 @@ 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 androidx.annotation.RequiresApi +import com.google.android.material.snackbar.Snackbar +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.appcompat.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.dialogs.BrowserDialogFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper -import com.kunzisoft.keepass.activities.helpers.KeyFileHelper +import com.kunzisoft.keepass.activities.helpers.OpenFileHelper import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter +import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.element.Database 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.MenuUtil import com.kunzisoft.keepass.utils.UriUtil +import com.kunzisoft.keepass.view.asError +import kotlinx.android.synthetic.main.activity_file_selection.* 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 { + AssignMasterKeyDialogFragment.AssignPasswordDialogListener { // Views private var fileListContainer: View? = null @@ -92,18 +78,18 @@ class FileDatabaseSelectActivity : StylishActivity(), // Adapter to manage database history list private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null - private var mFileDatabaseHistory: FileDatabaseHistory? = null + private var mFileDatabaseHistoryAction: FileDatabaseHistoryAction? = null private var mDatabaseFileUri: Uri? = null - private var mKeyFileHelper: KeyFileHelper? = null + private var mOpenFileHelper: OpenFileHelper? = null private var mDefaultPath: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mFileDatabaseHistory = FileDatabaseHistory.getInstance(WeakReference(applicationContext)) + mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext) setContentView(R.layout.activity_file_selection) fileListContainer = findViewById(R.id.container_file_list) @@ -131,10 +117,6 @@ class FileDatabaseSelectActivity : StylishActivity(), fileSelectExpandableLayout?.expand() } - // History list - val databaseFileListView = findViewById(R.id.file_list) - databaseFileListView.layoutManager = LinearLayoutManager(this) - // Open button openButtonView = findViewById(R.id.open_database) openButtonView?.setOnClickListener { _ -> @@ -143,101 +125,121 @@ class FileDatabaseSelectActivity : StylishActivity(), if (fileName.isEmpty()) fileName = it } - launchPasswordActivityWithPath(fileName) + UriUtil.parse(fileName)?.let { fileNameUri -> + launchPasswordActivityWithPath(fileNameUri) + } ?: run { + Log.e(TAG, "Unable to open the database link") + Snackbar.make(activity_file_selection_coordinator_layout, getString(R.string.error_can_not_handle_uri), Snackbar.LENGTH_LONG).asError().show() + null + } } // Create button createButtonView = findViewById(R.id.create_database) - createButtonView?.setOnClickListener { openCreateFileDialogFragmentWithPermissionCheck() } + if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/x-keepass" + }.resolveActivity(packageManager) == null) { + // No Activity found that can handle this intent. + createButtonView?.visibility = View.GONE + } + else{ + // There is an activity which can handle this intent. + createButtonView?.visibility = View.VISIBLE + } + + createButtonView?.setOnClickListener { createNewFile() } - mKeyFileHelper = KeyFileHelper(this) + mOpenFileHelper = OpenFileHelper(this) browseButtonView = findViewById(R.id.browse_button) - browseButtonView?.setOnClickListener(mKeyFileHelper!!.getOpenFileOnClickViewListener { - Uri.parse("file://" + openFileNameView!!.text.toString()) + browseButtonView?.setOnClickListener(mOpenFileHelper!!.getOpenFileOnClickViewListener { + UriUtil.parse(openFileNameView?.text?.toString()) }) + // History list + val fileDatabaseHistoryRecyclerView = findViewById(R.id.file_list) + fileDatabaseHistoryRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false) + // Removes blinks + (fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false // Construct adapter with listeners - mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this@FileDatabaseSelectActivity, - mFileDatabaseHistory?.databaseUriList ?: ArrayList()) - mAdapterDatabaseHistory?.setOnItemClickListener(this) - mAdapterDatabaseHistory?.setFileSelectClearListener(this) - mAdapterDatabaseHistory?.setFileInformationShowListener(this) - databaseFileListView.adapter = mAdapterDatabaseHistory + mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this) + mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen -> + UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri -> + launchPasswordActivity( + databaseFileUri, + UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri)) + } + updateFileListVisibility() + } + mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete -> + // Remove from app database + mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted -> + // Remove from adapter + fileHistoryDeleted?.let { databaseFileHistoryDeleted -> + mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted) + mAdapterDatabaseHistory?.notifyDataSetChanged() + updateFileListVisibility() + } + } + true + } + mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias -> + mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias) + } + fileDatabaseHistoryRecyclerView.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 != null && fileName.isNotEmpty()) { - val dbUri = UriUtil.parseUriFile(fileName) - var scheme: String? = null - if (dbUri != null) - scheme = dbUri.scheme - - if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) { - val path = dbUri!!.path - val db = File(path!!) + val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "") - if (db.exists()) { - launchPasswordActivityWithPath(path) - } - } else { - if (dbUri != null) - launchPasswordActivityWithPath(dbUri.toString()) - } + UriUtil.parse(databasePath)?.let { databaseFileUri -> + launchPasswordActivityWithPath(databaseFileUri) + } ?: run { + Log.i(TAG, "Unable to launch Password Activity") } } - Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) } + // Retrieve the database URI provided by file manager after an orientation change + if (savedInstanceState != null + && savedInstanceState.containsKey(EXTRA_DATABASE_URI)) { + mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI) + } } - 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) - } - } - )) - ; + /** + * Create a new file by calling the content provider + */ + @SuppressLint("InlinedApi") + private fun createNewFile() { + try { + startActivityForResult(Intent( + Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/x-keepass" + putExtra(Intent.EXTRA_TITLE, getString(R.string.database_file_name_default) + + getString(R.string.database_file_extension_default)) + }, + CREATE_FILE_REQUEST_CODE) + } catch (e: Exception) { + BrowserDialogFragment().show(supportFragmentManager, "browserDialog") + } } private fun fileNoFoundAction(e: FileNotFoundException) { val error = getString(R.string.file_not_found_content) - Toast.makeText(this@FileDatabaseSelectActivity, - error, Toast.LENGTH_LONG).show() + Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show() Log.e(TAG, error, e) } - private fun launchPasswordActivity(fileName: String, keyFile: String) { + private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) { EntrySelectionHelper.doEntrySelectionAction(intent, { try { PasswordActivity.launch(this@FileDatabaseSelectActivity, - fileName, keyFile) + databaseUri, keyFile) } catch (e: FileNotFoundException) { fileNoFoundAction(e) } @@ -245,7 +247,7 @@ class FileDatabaseSelectActivity : StylishActivity(), { try { PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity, - fileName, keyFile) + databaseUri, keyFile) finish() } catch (e: FileNotFoundException) { fileNoFoundAction(e) @@ -255,7 +257,7 @@ class FileDatabaseSelectActivity : StylishActivity(), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, - fileName, keyFile, + databaseUri, keyFile, assistStructure) } catch (e: FileNotFoundException) { fileNoFoundAction(e) @@ -265,8 +267,8 @@ class FileDatabaseSelectActivity : StylishActivity(), }) } - private fun launchPasswordActivityWithPath(path: String) { - launchPasswordActivity(path, "") + private fun launchPasswordActivityWithPath(databaseUri: Uri) { + launchPasswordActivity(databaseUri, null) // Delete flickering for kitkat <= if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) overridePendingTransition(0, 0) @@ -295,26 +297,23 @@ class FileDatabaseSelectActivity : StylishActivity(), super.onResume() updateExternalStorageWarning() - updateFileListVisibility() - mAdapterDatabaseHistory!!.notifyDataSetChanged() + + // Construct adapter with listeners + mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList -> + databaseFileHistoryList?.let { + mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it) + 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, 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") + // to retrieve the URI of a created database after an orientation change + outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri) } private fun updateFileListVisibility() { @@ -324,88 +323,12 @@ class FileDatabaseSelectActivity : StylishActivity(), 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, "UTF-8") - // 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 { - UriUtil.parseUriFile(mDatabaseFileUri)?.let { databaseUri -> + mDatabaseFileUri?.let { databaseUri -> // Create the new database ProgressDialogThread(this@FileDatabaseSelectActivity, @@ -418,22 +341,21 @@ class FileDatabaseSelectActivity : StylishActivity(), keyFileChecked, keyFile, true, // TODO get readonly - LaunchGroupActivityFinish(databaseUri) + LaunchGroupActivityFinish(databaseUri, keyFile) ) }, 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() + val error = getString(R.string.error_create_database_file) + Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show() + Log.e(TAG, error, e) } } - private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() { + private inner class LaunchGroupActivityFinish(private val databaseFileUri: Uri, + private val keyFileUri: Uri?) : ActionRunnable() { override fun run() { finishRun(true, null) @@ -443,7 +365,7 @@ class FileDatabaseSelectActivity : StylishActivity(), runOnUiThread { if (result.isSuccess) { // Add database to recent files - mFileDatabaseHistory?.addDatabaseUri(fileURI) + mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseFileUri, keyFileUri) mAdapterDatabaseHistory?.notifyDataSetChanged() updateFileListVisibility() GroupActivity.launch(this@FileDatabaseSelectActivity) @@ -460,29 +382,6 @@ class FileDatabaseSelectActivity : StylishActivity(), } - 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) @@ -490,44 +389,73 @@ class FileDatabaseSelectActivity : StylishActivity(), AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) } - mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data + mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data ) { uri -> if (uri != null) { if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) { - launchPasswordActivityWithPath(uri.toString()) + launchPasswordActivityWithPath(uri) } 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() + // Retrieve the created URI from the file manager + if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + mDatabaseFileUri = data?.data + if (mDatabaseFileUri != null) { + AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog") + } + // else { + // TODO Show error + // } + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) MenuUtil.defaultMenuInflater(menuInflater, menu) + + Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) } + return true } + private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) { + // If no recent files + val createDatabaseEducationPerformed = createButtonView != null && createButtonView!!.visibility == View.VISIBLE + && mAdapterDatabaseHistory != null + && mAdapterDatabaseHistory!!.itemCount > 0 + && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation( + createButtonView!!, + { + createNewFile() + }, + { + // But if the user cancel, it can also select a database + performedNextEducation(fileDatabaseSelectActivityEducation) + }) + if (!createDatabaseEducationPerformed) { + // selectDatabaseEducationPerformed + browseButtonView != null + && fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( + browseButtonView!!, + {tapTargetView -> + tapTargetView?.let { + mOpenFileHelper?.openFileOnClickViewListener?.onClick(it) + } + }, + { + fileSelectExpandableButtonView?.let { + fileDatabaseSelectActivityEducation + .checkAndPerformedOpenLinkDatabaseEducation(it) + } + } + ) + } + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item) } @@ -536,6 +464,9 @@ class FileDatabaseSelectActivity : StylishActivity(), private const val TAG = "FileDbSelectActivity" private const val EXTRA_STAY = "EXTRA_STAY" + private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI" + + private const val CREATE_FILE_REQUEST_CODE = 3853 /* * ------------------------- @@ -550,7 +481,7 @@ class FileDatabaseSelectActivity : StylishActivity(), */ fun launchForKeyboardSelection(activity: Activity) { - KeyboardHelper.startActivityForKeyboardSelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java)) + EntrySelectionHelper.startActivityForEntrySelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java)) } /* diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index e85733efd..1b5a29882 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -29,11 +29,10 @@ import android.graphics.Color import android.os.Build import android.os.Bundle import android.os.Handler -import android.preference.PreferenceManager -import android.support.annotation.RequiresApi -import android.support.v4.app.FragmentManager -import android.support.v7.widget.SearchView -import android.support.v7.widget.Toolbar +import androidx.annotation.RequiresApi +import androidx.fragment.app.FragmentManager +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.Toolbar import android.util.Log import android.view.Menu import android.view.MenuItem @@ -58,7 +57,6 @@ import com.kunzisoft.keepass.database.action.node.ActionNodeDatabaseRunnable.Com import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.icons.assignDatabaseIcon -import com.kunzisoft.keepass.magikeyboard.KeyboardHelper import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -178,12 +176,12 @@ class GroupActivity : LockingActivity(), // Initialize the fragment with the list mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment? if (mListNodesFragment == null) - mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, readOnly, mCurrentGroupIsASearch) + mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, mCurrentGroupIsASearch) // Attach fragment to content view supportFragmentManager.beginTransaction().replace( R.id.nodes_list_fragment_container, - mListNodesFragment, + mListNodesFragment!!, fragmentTag) .commit() @@ -207,15 +205,19 @@ class GroupActivity : LockingActivity(), Log.i(TAG, "Finished creating tree") } - override fun onNewIntent(intent: Intent) { - Log.d(TAG, "setNewIntent: $intent") - setIntent(intent) - mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intent.action) { - // only one instance of search in backstack - openSearchGroup(retrieveCurrentGroup(intent, null)) - true - } else { - false + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + intent?.let { intentNotNull -> + Log.d(TAG, "setNewIntent: $intentNotNull") + setIntent(intentNotNull) + mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) { + // only one instance of search in backstack + openSearchGroup(retrieveCurrentGroup(intentNotNull, null)) + true + } else { + false + } } } @@ -239,7 +241,7 @@ class GroupActivity : LockingActivity(), // Check TimeoutHelper TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) { // Open a group in a new fragment - val newListNodeFragment = ListNodesFragment.newInstance(group, readOnly, isASearch) + val newListNodeFragment = ListNodesFragment.newInstance(group, mReadOnly, isASearch) val fragmentTransaction = supportFragmentManager.beginTransaction() // Different animation val fragmentTag: String @@ -283,6 +285,9 @@ class GroupActivity : LockingActivity(), private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): GroupVersioned? { + // Force read only if the database is like that + mReadOnly = mDatabase?.isReadOnly == true || mReadOnly + // If it's a search if (Intent.ACTION_SEARCH == intent.action) { return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' }) @@ -297,8 +302,6 @@ class GroupActivity : LockingActivity(), pwGroupId = intent.getParcelableExtra(GROUP_ID_KEY) } - readOnly = mDatabase?.isReadOnly == true || readOnly // Force read only if the database is like that - Log.w(TAG, "Creating tree view") val currentGroup: GroupVersioned? currentGroup = if (pwGroupId == null) { @@ -357,7 +360,7 @@ class GroupActivity : LockingActivity(), } // Show selection mode message if needed - if (selectionMode) { + if (mSelectionMode) { modeTitleView?.visibility = View.VISIBLE } else { modeTitleView?.visibility = View.GONE @@ -367,8 +370,8 @@ class GroupActivity : LockingActivity(), addNodeButtonView?.apply { // To enable add button - val addGroupEnabled = !readOnly && !mCurrentGroupIsASearch - var addEntryEnabled = !readOnly && !mCurrentGroupIsASearch + val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch + var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch mCurrentGroup?.let { val isRoot = it == mRootGroup if (!it.allowAddEntryIfIsRoot()) @@ -401,7 +404,7 @@ class GroupActivity : LockingActivity(), val entryVersioned = node as EntryVersioned EntrySelectionHelper.doEntrySelectionAction(intent, { - EntryActivity.launch(this@GroupActivity, entryVersioned, readOnly) + EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly) }, { // Populate Magikeyboard with entry @@ -481,7 +484,7 @@ class GroupActivity : LockingActivity(), entryToCopy, newParent, AfterAddNodeRunnable(), - !readOnly) + !mReadOnly) }.start() } @@ -526,7 +529,7 @@ class GroupActivity : LockingActivity(), groupToMove, newParent, AfterAddNodeRunnable(), - !readOnly) + !mReadOnly) }.start() } @@ -538,7 +541,7 @@ class GroupActivity : LockingActivity(), entryToMove, newParent, AfterAddNodeRunnable(), - !readOnly) + !mReadOnly) }.start() } @@ -558,7 +561,7 @@ class GroupActivity : LockingActivity(), Database.getInstance(), group, AfterDeleteNodeRunnable(), - !readOnly) + !mReadOnly) }.start() } @@ -569,7 +572,7 @@ class GroupActivity : LockingActivity(), Database.getInstance(), entry, AfterDeleteNodeRunnable(), - !readOnly) + !mReadOnly) }.start() } @@ -586,7 +589,7 @@ class GroupActivity : LockingActivity(), val inflater = menuInflater inflater.inflate(R.menu.search, menu) inflater.inflate(R.menu.database_lock, menu) - if (!selectionMode) { + if (!mSelectionMode) { inflater.inflate(R.menu.default_menu, menu) MenuUtil.contributionMenuInflater(inflater, menu) } @@ -628,8 +631,9 @@ class GroupActivity : LockingActivity(), private fun performedNextEducation(groupActivityEducation: GroupActivityEducation, menu: Menu) { + // If no node, show education to add new one - if (mListNodesFragment != null + val addNodeButtonEducationPerformed = mListNodesFragment != null && mListNodesFragment!!.isEmpty && addNodeButtonView?.addButtonView != null && addNodeButtonView!!.isEnable @@ -641,71 +645,47 @@ class GroupActivity : LockingActivity(), { performedNextEducation(groupActivityEducation, menu) } - )) - else if (toolbar != null - && toolbar!!.findViewById(R.id.menu_search) != null - && groupActivityEducation.checkAndPerformedSearchMenuEducation( - toolbar!!.findViewById(R.id.menu_search), - { - menu.findItem(R.id.menu_search).expandActionView() - }, - { - performedNextEducation(groupActivityEducation, menu) - })) - else if (toolbar != null - && toolbar!!.findViewById(R.id.menu_sort) != null - && groupActivityEducation.checkAndPerformedSortMenuEducation( + ) + if (!addNodeButtonEducationPerformed) { + + val searchMenuEducationPerformed = toolbar != null + && toolbar!!.findViewById(R.id.menu_search) != null + && groupActivityEducation.checkAndPerformedSearchMenuEducation( + toolbar!!.findViewById(R.id.menu_search), + { + menu.findItem(R.id.menu_search).expandActionView() + }, + { + performedNextEducation(groupActivityEducation, menu) + }) + + if (!searchMenuEducationPerformed) { + + val sortMenuEducationPerformed = toolbar != null + && toolbar!!.findViewById(R.id.menu_sort) != null + && groupActivityEducation.checkAndPerformedSortMenuEducation( toolbar!!.findViewById(R.id.menu_sort), { onOptionsItemSelected(menu.findItem(R.id.menu_sort)) }, { performedNextEducation(groupActivityEducation, menu) - })) - else if (toolbar != null - && toolbar!!.findViewById(R.id.menu_lock) != null - && groupActivityEducation.checkAndPerformedLockMenuEducation( - toolbar!!.findViewById(R.id.menu_lock), - { - onOptionsItemSelected(menu.findItem(R.id.menu_lock)) - }, - { - performedNextEducation(groupActivityEducation, menu) - })) - ; - } - - override fun startActivity(intent: Intent) { - - // Get the intent, verify the action and get the query - if (Intent.ACTION_SEARCH == intent.action) { - val query = intent.getStringExtra(SearchManager.QUERY) - // manually launch the real search activity - val searchIntent = Intent(applicationContext, GroupActivity::class.java) - // add query to the Intent Extras - searchIntent.action = Intent.ACTION_SEARCH - searchIntent.putExtra(SearchManager.QUERY, query) + }) - EntrySelectionHelper.doEntrySelectionAction(intent, - { - super@GroupActivity.startActivity(intent) - }, - { - KeyboardHelper.startActivityForKeyboardSelection( - this@GroupActivity, - searchIntent) - finish() - }, - { assistStructure -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.startActivityForAutofillResult( - this@GroupActivity, - searchIntent, - assistStructure) - } - }) - } else { - super.startActivity(intent) + if (!sortMenuEducationPerformed) { + // lockMenuEducationPerformed + toolbar != null + && toolbar!!.findViewById(R.id.menu_lock) != null + && groupActivityEducation.checkAndPerformedLockMenuEducation( + toolbar!!.findViewById(R.id.menu_lock), + { + onOptionsItemSelected(menu.findItem(R.id.menu_lock)) + }, + { + performedNextEducation(groupActivityEducation, menu) + }) + } + } } } @@ -724,7 +704,7 @@ class GroupActivity : LockingActivity(), } else -> { // Check the time lock before launching settings - MenuUtil.onDefaultMenuOptionsItemSelected(this, item, readOnly, true) + MenuUtil.onDefaultMenuOptionsItemSelected(this, item, mReadOnly, true) return super.onOptionsItemSelected(item) } } @@ -754,7 +734,7 @@ class GroupActivity : LockingActivity(), newGroup, currentGroup, AfterAddNodeRunnable(), - !readOnly) + !mReadOnly) }.start() } } @@ -776,7 +756,7 @@ class GroupActivity : LockingActivity(), oldGroupToUpdate, updateGroup, AfterUpdateNodeRunnable(), - !readOnly) + !mReadOnly) }.start() } } @@ -857,11 +837,9 @@ class GroupActivity : LockingActivity(), } private fun showWarnings() { - // TODO Preferences if (Database.getInstance().isReadOnly) { - val prefs = PreferenceManager.getDefaultSharedPreferences(this) - if (prefs.getBoolean(getString(R.string.show_read_only_warning), true)) { - ReadOnlyDialog(this).show() + if (PreferencesUtil.showReadOnlyWarning(this)) { + ReadOnlyDialog().show(supportFragmentManager, "readOnlyDialog") } } } @@ -870,18 +848,25 @@ class GroupActivity : LockingActivity(), mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) + override fun startActivity(intent: Intent) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) - } + // Get the intent, verify the action and get the query + if (Intent.ACTION_SEARCH == intent.action) { + // manually launch the real search activity + val searchIntent = Intent(applicationContext, GroupActivity::class.java).apply { + // Add bundle of current intent + putExtras(this@GroupActivity.intent) + // add query to the Intent Extras + action = Intent.ACTION_SEARCH + putExtra(SearchManager.QUERY, intent.getStringExtra(SearchManager.QUERY)) + } - // Not directly get the entry from intent data but from database - mListNodesFragment?.rebuildList() + super.startActivity(searchIntent) + } else { + super.startActivity(intent) + } } - @SuppressLint("RestrictedApi") override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) { /* * ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in @@ -894,9 +879,18 @@ class GroupActivity : LockingActivity(), intent.flags = flags } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - super.startActivityForResult(intent, requestCode, options) + super.startActivityForResult(intent, requestCode, options) + } + + 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) } + + // Not directly get the entry from intent data but from database + mListNodesFragment?.rebuildList() } private fun removeSearchInIntent(intent: Intent) { @@ -973,10 +967,10 @@ class GroupActivity : LockingActivity(), */ // TODO implement pre search to directly open the direct group - fun launchForKeyboardSelection(activity: Activity, readOnly: Boolean) { + fun launchForKeyboarSelection(activity: Activity, readOnly: Boolean) { TimeoutHelper.recordTime(activity) buildAndLaunchIntent(activity, null, readOnly) { intent -> - KeyboardHelper.startActivityForKeyboardSelection(activity, intent) + EntrySelectionHelper.startActivityForEntrySelection(activity, intent) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt index 618f3bb0b..3346deb1f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt @@ -5,8 +5,8 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import android.util.Log import android.view.LayoutInflater import android.view.Menu @@ -53,31 +53,31 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis val isEmpty: Boolean get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0 - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) try { - nodeClickCallback = context as NodeAdapter.NodeClickCallback? + nodeClickCallback = context as NodeAdapter.NodeClickCallback } catch (e: ClassCastException) { // The activity doesn't implement the interface, throw exception - throw ClassCastException(context?.toString() + throw ClassCastException(context.toString() + " must implement " + NodeAdapter.NodeClickCallback::class.java.name) } try { - nodeMenuListener = context as NodeAdapter.NodeMenuListener? + nodeMenuListener = context as NodeAdapter.NodeMenuListener } catch (e: ClassCastException) { nodeMenuListener = null // Context menu can be omit - Log.w(TAG, context?.toString() + Log.w(TAG, context.toString() + " must implement " + NodeAdapter.NodeMenuListener::class.java.name) } try { - onScrollListener = context as OnScrollListener? + onScrollListener = context as OnScrollListener } catch (e: ClassCastException) { onScrollListener = null // Context menu can be omit - Log.w(TAG, context?.toString() + Log.w(TAG, context.toString() + " must implement " + RecyclerView.OnScrollListener::class.java.name) } } @@ -130,7 +130,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis onScrollListener?.let { onScrollListener -> listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) onScrollListener.onScrolled(dy) } @@ -195,14 +195,14 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis } } - override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { - inflater?.inflate(R.menu.tree, menu) + 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) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { R.id.menu_sort -> { context?.let { context -> diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt index 638e99320..1df82893d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -19,56 +19,59 @@ */ 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 androidx.annotation.RequiresApi +import com.google.android.material.snackbar.Snackbar +import androidx.appcompat.widget.Toolbar import android.text.Editable import android.text.TextWatcher +import android.util.Log import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.View import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.widget.* +import androidx.biometric.BiometricManager import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment -import com.kunzisoft.keepass.activities.helpers.* +import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper +import com.kunzisoft.keepass.activities.helpers.OpenFileHelper +import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity +import com.kunzisoft.keepass.app.database.CipherDatabaseAction +import com.kunzisoft.keepass.app.database.CipherDatabaseEntity +import com.kunzisoft.keepass.utils.FileDatabaseInfo +import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction 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.FingerPrintHelper -import com.kunzisoft.keepass.fingerprint.FingerPrintViewsManager -import com.kunzisoft.keepass.magikeyboard.KeyboardHelper +import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil -import com.kunzisoft.keepass.view.FingerPrintInfoView -import permissions.dispatcher.* -import java.io.File +import com.kunzisoft.keepass.view.AdvancedUnlockInfoView +import com.kunzisoft.keepass.view.asError +import kotlinx.android.synthetic.main.activity_password.* import java.io.FileNotFoundException import java.lang.ref.WeakReference -@RuntimePermissions -class PasswordActivity : StylishActivity(), - UriIntentInitTaskCallback { +class PasswordActivity : StylishActivity() { // Views private var toolbar: Toolbar? = null @@ -80,18 +83,18 @@ class PasswordActivity : StylishActivity(), private var checkboxPasswordView: CompoundButton? = null private var checkboxKeyFileView: CompoundButton? = null private var checkboxDefaultDatabaseView: CompoundButton? = null - private var fingerPrintInfoView: FingerPrintInfoView? = null + private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null private var mDatabaseFileUri: Uri? = null private var prefs: SharedPreferences? = null private var mRememberKeyFile: Boolean = false - private var mKeyFileHelper: KeyFileHelper? = null + private var mOpenFileHelper: OpenFileHelper? = null private var readOnly: Boolean = false - private var fingerPrintViewsManager: FingerPrintViewsManager? = null + private var advancedUnlockedManager: AdvancedUnlockedManager? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -116,13 +119,13 @@ class PasswordActivity : StylishActivity(), checkboxPasswordView = findViewById(R.id.password_checkbox) checkboxKeyFileView = findViewById(R.id.keyfile_checkox) checkboxDefaultDatabaseView = findViewById(R.id.default_database) - fingerPrintInfoView = findViewById(R.id.fingerprint_info) + advancedUnlockInfoView = findViewById(R.id.fingerprint_info) readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) val browseView = findViewById(R.id.browse_button) - mKeyFileHelper = KeyFileHelper(this@PasswordActivity) - browseView.setOnClickListener(mKeyFileHelper!!.openFileOnClickViewListener) + mOpenFileHelper = OpenFileHelper(this@PasswordActivity) + browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener) passwordView?.setOnEditorActionListener(onEditorActionListener) passwordView?.addTextChangedListener(object : TextWatcher { @@ -172,8 +175,7 @@ class PasswordActivity : StylishActivity(), // For check shutdown super.onResume() - UriIntentInitTask(WeakReference(this), this, mRememberKeyFile) - .execute(intent) + initUriFromIntent() } override fun onSaveInstanceState(outState: Bundle) { @@ -181,26 +183,51 @@ class PasswordActivity : StylishActivity(), super.onSaveInstanceState(outState) } - override fun onPostInitTask(databaseFileUri: Uri?, keyFileUri: Uri?, errorStringId: Int?) { - mDatabaseFileUri = databaseFileUri + private fun initUriFromIntent() { + + val databaseUri: Uri? + val keyFileUri: Uri? + + // If is a view intent + val action = intent.action + if (action != null && action == VIEW_INTENT) { + + val databaseUriRetrieve = intent.data + // Stop activity here if we can't verify database URI + if (!UriUtil.verifyFileUri(databaseUriRetrieve)) { + Log.e(TAG, "File URI not validate") + finish() + } + databaseUri = databaseUriRetrieve + keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE) + + } else { + databaseUri = intent.getParcelableExtra(KEY_FILENAME) + keyFileUri = intent.getParcelableExtra(KEY_KEYFILE) + } - if (errorStringId != null) { - Toast.makeText(this@PasswordActivity, errorStringId, Toast.LENGTH_LONG).show() - finish() - return + // Post init uri with KeyFile if needed + if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) { + // Retrieve KeyFile in a thread + databaseUri?.let { databaseUriNotNull -> + FileDatabaseHistoryAction.getInstance(applicationContext) + .getKeyFileUriByDatabaseUri(databaseUriNotNull) { + onPostInitUri(databaseUri, it) + } + } + } else { + onPostInitUri(databaseUri, keyFileUri) } + } - // Verify permission to read file - if (databaseFileUri != null && !databaseFileUri.scheme!!.contains("content")) - doNothingWithPermissionCheck() + private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) { + mDatabaseFileUri = databaseFileUri // Define title - val dbUriString = databaseFileUri?.toString() ?: "" - if (dbUriString.isNotEmpty()) { - if (PreferencesUtil.isFullFilePathEnable(this)) - filenameView?.text = dbUriString - else - filenameView?.text = File(databaseFileUri!!.path!!).name // TODO Encapsulate + databaseFileUri?.let { + FileDatabaseInfo(this, it).retrieveDatabaseTitle { title -> + filenameView?.text = title + } } // Define Key File text @@ -211,14 +238,16 @@ class PasswordActivity : StylishActivity(), // Define listeners for default database checkbox and validate button checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked -> - var newDefaultFileName = "" + var newDefaultFileName: Uri? = null if (isChecked) { - newDefaultFileName = databaseFileUri?.toString() ?: newDefaultFileName + newDefaultFileName = databaseFileUri ?: newDefaultFileName } - prefs?.edit()?.apply() { - putString(KEY_DEFAULT_FILENAME, newDefaultFileName) - apply() + newDefaultFileName?.let { + prefs?.edit()?.apply { + putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString()) + apply() + } } val backupManager = BackupManager(this@PasswordActivity) @@ -227,10 +256,10 @@ class PasswordActivity : StylishActivity(), confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() } // Retrieve settings for default database - val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "") + val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "") if (databaseFileUri != null && databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty() - && databaseFileUri == UriUtil.parseUriFile(defaultFilename)) { + && databaseFileUri == UriUtil.parse(defaultFilename)) { checkboxDefaultDatabaseView?.isChecked = true } @@ -247,30 +276,42 @@ class PasswordActivity : StylishActivity(), // Init FingerPrint elements var fingerPrintInit = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (PreferencesUtil.isFingerprintEnable(this)) { - if (fingerPrintViewsManager == null) { - fingerPrintViewsManager = FingerPrintViewsManager(this, + if (PreferencesUtil.isBiometricUnlockEnable(this)) { + + advancedUnlockInfoView?.setOnClickListener { + FingerPrintExplanationDialog().show(supportFragmentManager, "fingerPrintExplanationDialog") + } + + if (advancedUnlockedManager == null && databaseFileUri != null) { + advancedUnlockedManager = AdvancedUnlockedManager(this, databaseFileUri, - fingerPrintInfoView, + advancedUnlockInfoView, checkboxPasswordView, enableButtonOnCheckedChangeListener, - passwordView) { passwordRetrieve -> - // Load the database if password is registered or retrieve - passwordRetrieve?.let { + passwordView, + { passwordEncrypted, ivSpec -> + // Load the database if password is registered with biometric + if (passwordEncrypted != null && ivSpec != null) { + verifyCheckboxesAndLoadDatabase( + CipherDatabaseEntity( + databaseFileUri.toString(), + passwordEncrypted, + ivSpec) + ) + } + }, + { passwordDecrypted -> + // Load the database if password is retrieve from biometric + passwordDecrypted?.let { // Retrieve from fingerprint verifyKeyFileCheckboxAndLoadDatabase(it) - } ?: run { - // Register with fingerprint - verifyCheckboxesAndLoadDatabase() } - } + }) } - fingerPrintViewsManager?.initFingerprint() - // checks if fingerprint is available, will also start listening for fingerprints when available - fingerPrintViewsManager?.checkFingerprintAvailability() + advancedUnlockedManager?.initBiometric() fingerPrintInit = true } else { - fingerPrintViewsManager?.destroy() + advancedUnlockedManager?.destroy() } } if (!fingerPrintInit) { @@ -328,27 +369,34 @@ class PasswordActivity : StylishActivity(), override fun onPause() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - fingerPrintViewsManager?.stopListening() + advancedUnlockedManager?.pause() } super.onPause() } override fun onDestroy() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - fingerPrintViewsManager?.destroy() + advancedUnlockedManager?.destroy() } super.onDestroy() } - private fun verifyCheckboxesAndLoadDatabase(password: String? = passwordView?.text?.toString(), - keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) { + private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) { + val password: String? = passwordView?.text?.toString() + val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString()) + verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity) + } + + private fun verifyCheckboxesAndLoadDatabase(password: String?, + keyFile: Uri?, + cipherDatabaseEntity: CipherDatabaseEntity? = null) { val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile - loadDatabase(keyPassword, keyFileUri) + loadDatabase(keyPassword, keyFileUri, cipherDatabaseEntity) } - private fun verifyKeyFileCheckboxAndLoadDatabase(password: String? = passwordView?.text?.toString(), - keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) { + private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) { + val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString()) val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile loadDatabase(password, keyFileUri) } @@ -358,10 +406,12 @@ class PasswordActivity : StylishActivity(), checkboxPasswordView?.isChecked = false } - private fun loadDatabase(password: String?, keyFile: Uri?) { + private fun loadDatabase(password: String?, keyFile: Uri?, cipherDatabaseEntity: CipherDatabaseEntity? = null) { - if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { - removePassword() + runOnUiThread { + if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { + removePassword() + } } // Clear before we load @@ -379,7 +429,7 @@ class PasswordActivity : StylishActivity(), password, keyFile, progressTaskUpdater, - AfterLoadingDatabase(database, password)) + AfterLoadingDatabase(database, password, cipherDatabaseEntity)) }, R.string.loading_database).start() } @@ -388,17 +438,17 @@ class PasswordActivity : StylishActivity(), /** * Called after verify and try to opening the database */ - private inner class AfterLoadingDatabase internal constructor(var database: Database, - val password: String?) + private inner class AfterLoadingDatabase(val database: Database, val password: String?, + val cipherDatabaseEntity: CipherDatabaseEntity? = null) : ActionRunnable() { override fun onFinishRun(result: Result) { runOnUiThread { // Recheck fingerprint if error if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (PreferencesUtil.isFingerprintEnable(this@PasswordActivity)) { - // Stay with the same mode - fingerPrintViewsManager?.reInitWithFingerprintMode() + if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) { + // Stay with the same mode and init it + advancedUnlockedManager?.initBiometricMode() } } @@ -406,32 +456,45 @@ class PasswordActivity : StylishActivity(), // Remove the password in view in all cases removePassword() - if (database.validatePasswordEncoding(password)) { - launchGroupActivity() + // Register the biometric + if (cipherDatabaseEntity != null) { + CipherDatabaseAction.getInstance(this@PasswordActivity) + .addOrUpdateCipherDatabase(cipherDatabaseEntity) { + checkAndLaunchGroupActivity(database, password) + } } else { - PasswordEncodingDialogFragment().apply { - positiveButtonClickListener = DialogInterface.OnClickListener { _, _ -> - launchGroupActivity() - } - show(supportFragmentManager, "passwordEncodingTag") - } + checkAndLaunchGroupActivity(database, password) } + } else { if (result.message != null && result.message!!.isNotEmpty()) { - Toast.makeText(this@PasswordActivity, result.message, Toast.LENGTH_LONG).show() + Snackbar.make(activity_password_coordinator_layout, result.message!!, Snackbar.LENGTH_LONG).asError().show() } } } } } + private fun checkAndLaunchGroupActivity(database: Database, password: String?) { + if (database.validatePasswordEncoding(password)) { + launchGroupActivity() + } else { + PasswordEncodingDialogFragment().apply { + positiveButtonClickListener = DialogInterface.OnClickListener { _, _ -> + launchGroupActivity() + } + show(supportFragmentManager, "passwordEncodingTag") + } + } + } + private fun launchGroupActivity() { EntrySelectionHelper.doEntrySelectionAction(intent, { GroupActivity.launch(this@PasswordActivity, readOnly) }, { - GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly) + GroupActivity.launchForKeyboarSelection(this@PasswordActivity, readOnly) // Do not keep history finish() }, @@ -452,7 +515,7 @@ class PasswordActivity : StylishActivity(), if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Fingerprint menu - fingerPrintViewsManager?.inflateOptionsMenu(inflater, menu) + advancedUnlockedManager?.inflateOptionsMenu(inflater, menu) } super.onCreateOptionsMenu(menu) @@ -465,32 +528,41 @@ class PasswordActivity : StylishActivity(), private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation, menu: Menu) { - if (toolbar != null - && passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation( + val unlockEducationPerformed = toolbar != null + && passwordActivityEducation.checkAndPerformedUnlockEducation( toolbar!!, { performedNextEducation(passwordActivityEducation, menu) }, { performedNextEducation(passwordActivityEducation, menu) - })) - else if (toolbar != null - && toolbar!!.findViewById(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)) - && fingerPrintInfoView != null && fingerPrintInfoView?.fingerPrintImageView != null - && passwordActivityEducation.checkAndPerformedFingerprintEducation(fingerPrintInfoView?.fingerPrintImageView!!)) - ; + }) + if (!unlockEducationPerformed) { + + val readOnlyEducationPerformed = toolbar != null + && toolbar!!.findViewById(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) + }) + + if (!readOnlyEducationPerformed) { + + val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate() + // fingerprintEducationPerformed + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && PreferencesUtil.isBiometricUnlockEnable(applicationContext) + && (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) + && advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null + && passwordActivityEducation.checkAndPerformedFingerprintEducation(advancedUnlockInfoView?.unlockIconImageView!!) + + } + } } private fun changeOpenFileReadIcon(togglePassword: MenuItem) { @@ -512,7 +584,7 @@ class PasswordActivity : StylishActivity(), changeOpenFileReadIcon(item) } R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - fingerPrintViewsManager?.deleteEntryKey() + advancedUnlockedManager?.deleteEntryKey() } else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) } @@ -520,12 +592,6 @@ class PasswordActivity : StylishActivity(), return super.onOptionsItemSelected(item) } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, 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, @@ -538,7 +604,7 @@ class PasswordActivity : StylishActivity(), } var keyFileResult = false - mKeyFileHelper?.let { + mOpenFileHelper?.let { keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data ) { uri -> if (uri != null) { @@ -557,64 +623,28 @@ class PasswordActivity : StylishActivity(), } } - @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" + const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH" + + private const val KEY_FILENAME = "fileName" + private const val KEY_KEYFILE = "keyFile" + private const val VIEW_INTENT = "android.intent.action.VIEW" private const val KEY_PASSWORD = "password" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" - private fun buildAndLaunchIntent(activity: Activity, fileName: String, keyFile: String, + private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, intentBuildLauncher: (Intent) -> Unit) { val intent = Intent(activity, PasswordActivity::class.java) - intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName) - intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile) + intent.putExtra(KEY_FILENAME, databaseFile) + if (keyFile != null) + intent.putExtra(KEY_KEYFILE, keyFile) intentBuildLauncher.invoke(intent) } - @Throws(FileNotFoundException::class) - private fun verifyFileNameUriFromLaunch(fileName: String) { - if (fileName.isEmpty()) { - throw FileNotFoundException() - } - - val uri = UriUtil.parseUriFile(fileName) - val scheme = uri?.scheme - if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) { - val dbFile = File(uri.path!!) - if (!dbFile.exists()) { - throw FileNotFoundException() - } - } - } - /* * ------------------------- * Standard Launch @@ -624,10 +654,9 @@ class PasswordActivity : StylishActivity(), @Throws(FileNotFoundException::class) fun launch( activity: Activity, - fileName: String, - keyFile: String) { - verifyFileNameUriFromLaunch(fileName) - buildAndLaunchIntent(activity, fileName, keyFile) { intent -> + databaseFile: Uri, + keyFile: Uri?) { + buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> activity.startActivity(intent) } } @@ -641,12 +670,10 @@ class PasswordActivity : StylishActivity(), @Throws(FileNotFoundException::class) fun launchForKeyboardResult( activity: Activity, - fileName: String, - keyFile: String) { - verifyFileNameUriFromLaunch(fileName) - - buildAndLaunchIntent(activity, fileName, keyFile) { intent -> - KeyboardHelper.startActivityForKeyboardSelection(activity, intent) + databaseFile: Uri, + keyFile: Uri?) { + buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> + EntrySelectionHelper.startActivityForEntrySelection(activity, intent) } } @@ -660,20 +687,18 @@ class PasswordActivity : StylishActivity(), @Throws(FileNotFoundException::class) fun launchForAutofillResult( activity: Activity, - fileName: String, - keyFile: String, + databaseFile: Uri, + keyFile: Uri?, assistStructure: AssistStructure?) { - verifyFileNameUriFromLaunch(fileName) - if (assistStructure != null) { - buildAndLaunchIntent(activity, fileName, keyFile) { intent -> + buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> AutofillHelper.startActivityForAutofillResult( activity, intent, assistStructure) } } else { - launch(activity, fileName, keyFile) + launch(activity, databaseFile, keyFile) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt index 5059adea4..a71a8f09b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt @@ -25,16 +25,16 @@ import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.Bundle -import android.support.v4.app.DialogFragment -import android.support.v7.app.AlertDialog +import com.google.android.material.textfield.TextInputLayout +import androidx.fragment.app.DialogFragment +import androidx.appcompat.app.AlertDialog import android.text.Editable import android.text.TextWatcher import android.view.View import android.widget.CompoundButton import android.widget.TextView -import android.widget.Toast import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.helpers.KeyFileHelper +import com.kunzisoft.keepass.activities.helpers.OpenFileHelper import com.kunzisoft.keepass.utils.UriUtil class AssignMasterKeyDialogFragment : DialogFragment() { @@ -43,15 +43,39 @@ class AssignMasterKeyDialogFragment : DialogFragment() { 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 passwordView: TextView? = null + private var passwordRepeatTextInputLayout: TextInputLayout? = null + private var passwordRepeatView: TextView? = null + + private var keyFileTextInputLayout: TextInputLayout? = null private var keyFileCheckBox: CompoundButton? = null private var keyFileView: TextView? = null private var mListener: AssignPasswordDialogListener? = null - private var mKeyFileHelper: KeyFileHelper? = null + private var mOpenFileHelper: OpenFileHelper? = null + + private val passwordTextWatcher = 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) { + passwordCheckBox?.isChecked = true + } + } + + private val keyFileTextWatcher = 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 + } + } interface AssignPasswordDialogListener { fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?, @@ -60,12 +84,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() { keyFileChecked: Boolean, keyFile: Uri?) } - override fun onAttach(activity: Context?) { + override fun onAttach(activity: Context) { super.onAttach(activity) try { - mListener = activity as AssignPasswordDialogListener? + mListener = activity as AssignPasswordDialogListener } catch (e: ClassCastException) { - throw ClassCastException(activity?.toString() + throw ClassCastException(activity.toString() + " must implement " + AssignPasswordDialogListener::class.java.name) } } @@ -83,33 +107,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() { .setNegativeButton(R.string.cancel) { _, _ -> } passwordCheckBox = rootView?.findViewById(R.id.password_checkbox) - passView = rootView?.findViewById(R.id.pass_password) - passView?.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) { - passwordCheckBox?.isChecked = true - } - }) - passConfView = rootView?.findViewById(R.id.pass_conf_password) + passwordView = rootView?.findViewById(R.id.pass_password) + passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout) + passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password) + keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout) 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 - } - }) - mKeyFileHelper = KeyFileHelper(this) + mOpenFileHelper = OpenFileHelper(this) rootView?.findViewById(R.id.browse_button)?.setOnClickListener { view -> - mKeyFileHelper?.openFileOnClickViewListener?.onClick(view) } + mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) } val dialog = builder.create() @@ -149,20 +157,35 @@ class AssignMasterKeyDialogFragment : DialogFragment() { return super.onCreateDialog(savedInstanceState) } + override fun onResume() { + super.onResume() + + // To check checkboxes if a text is present + passwordView?.addTextChangedListener(passwordTextWatcher) + keyFileView?.addTextChangedListener(keyFileTextWatcher) + } + + override fun onPause() { + super.onPause() + + passwordView?.removeTextChangedListener(passwordTextWatcher) + keyFileView?.removeTextChangedListener(keyFileTextWatcher) + } + private fun verifyPassword(): Boolean { var error = false if (passwordCheckBox != null && passwordCheckBox!!.isChecked - && passView != null - && passConfView != null) { - mMasterPassword = passView!!.text.toString() - val confPassword = passConfView!!.text.toString() + && passwordView != null + && passwordRepeatView != null) { + mMasterPassword = passwordView!!.text.toString() + val confPassword = passwordRepeatView!!.text.toString() // Verify that passwords match if (mMasterPassword != confPassword) { error = true // Passwords do not match - Toast.makeText(context, R.string.error_pass_match, Toast.LENGTH_LONG).show() + passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match) } if (mMasterPassword == null || mMasterPassword!!.isEmpty()) { @@ -177,13 +200,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() { var error = false if (keyFileCheckBox != null && keyFileCheckBox!!.isChecked) { - val keyFile = UriUtil.parseUriFile(keyFileView?.text?.toString()) - mKeyFile = keyFile - // Verify that a keyfile is set - if (keyFile == null || keyFile.toString().isEmpty()) { + UriUtil.parse(keyFileView?.text?.toString())?.let { uri -> + mKeyFile = uri + } ?: run { error = true - Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show() + keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile) } } return error @@ -224,9 +246,9 @@ class AssignMasterKeyDialogFragment : DialogFragment() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data + mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data ) { uri -> - UriUtil.parseUriFile(uri)?.let { pathUri -> + uri?.let { pathUri -> keyFileCheckBox?.isChecked = true keyFileView?.text = pathUri.toString() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt index 80c9f04cb..a981e04d0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt @@ -21,11 +21,12 @@ 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 androidx.fragment.app.DialogFragment +import androidx.appcompat.app.AlertDialog import android.widget.Button +import android.widget.TextView import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.utils.Util +import com.kunzisoft.keepass.utils.UriUtil class BrowserDialogFragment : DialogFragment() { @@ -37,15 +38,18 @@ class BrowserDialogFragment : DialogFragment() { builder.setView(root) .setNegativeButton(R.string.cancel) { _, _ -> } - val market = root.findViewById