From e4b550afc19bd7593efcb7e919bf6796bf6de72a Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 4 Jul 2019 14:34:06 +0200 Subject: [PATCH 01/24] 4 spaces files harmonisation #184 --- .../kunzisoft/keepass/tests/AccentTest.java | 64 ++- .../kunzisoft/keepass/tests/OutputTests.java | 12 +- .../kunzisoft/keepass/tests/PwDateTest.java | 24 +- .../keepass/tests/PwEntryTestV3.java | 60 +- .../keepass/tests/PwEntryTestV4.java | 8 +- .../kunzisoft/keepass/tests/PwGroupTest.java | 28 +- .../com/kunzisoft/keepass/tests/TestUtil.java | 46 +- .../kunzisoft/keepass/tests/TypesTest.java | 332 +++++------ .../keepass/tests/crypto/AESTest.java | 120 ++-- .../keepass/tests/crypto/CipherTest.java | 134 ++--- .../keepass/tests/crypto/FinalKeyTest.java | 102 ++-- .../keepass/tests/database/DeleteEntry.java | 44 +- .../keepass/tests/database/EntryV4.java | 6 +- .../keepass/tests/database/Kdb3.java | 24 +- .../keepass/tests/database/Kdb3Twofish.java | 6 +- .../keepass/tests/database/Kdb4Header.java | 14 +- .../keepass/tests/database/SprEngineTest.java | 58 +- .../keepass/tests/database/TestData.java | 10 +- .../keepass/tests/search/SearchTest.java | 114 ++-- .../keepass/tests/stream/HashedBlock.java | 178 +++--- .../keepass/tests/utils/StringUtilTest.java | 44 +- .../java/com/kunzisoft/keepass/app/App.java | 76 +-- .../keepass/backup/SettingsBackupAgent.java | 22 +- .../kunzisoft/keepass/crypto/AESProvider.java | 18 +- .../keepass/crypto/CipherFactory.java | 102 ++-- .../keepass/crypto/NativeAESCipherSpi.java | 528 +++++++++--------- .../kunzisoft/keepass/crypto/NativeLib.java | 46 +- .../keepass/crypto/PwStreamCipherFactory.java | 68 +-- .../crypto/finalkey/AndroidFinalKey.java | 78 +-- .../keepass/crypto/finalkey/FinalKey.java | 4 +- .../crypto/finalkey/FinalKeyFactory.java | 28 +- .../crypto/finalkey/NativeFinalKey.java | 30 +- .../database/element/PwDatabaseV3.java | 330 +++++------ .../keepass/database/element/PwDate.java | 330 +++++------ .../dialogs/IconPickerDialogFragment.java | 136 ++--- .../keepass/dialogs/ReadOnlyDialog.java | 22 +- .../keepass/dialogs/WarningDialog.java | 86 +-- .../keepass/fileselect/BrowserDialog.java | 38 +- .../keepass/fileselect/FileDbHelper.java | 428 +++++++------- .../fileselect/FileSelectActivity.java | 484 ++++++++-------- .../keepass/icons/IconDrawableFactory.java | 196 +++---- .../magikeyboard/view/MagikeyboardView.java | 42 +- .../keepass/password/PasswordGenerator.java | 176 +++--- .../keepass/stream/CopyInputStream.java | 126 ++--- .../keepass/stream/CountInputStream.java | 94 ++-- .../keepass/stream/CountOutputStream.java | 60 +- .../stream/HashedBlockInputStream.java | 250 ++++----- .../stream/HashedBlockOutputStream.java | 206 +++---- .../keepass/stream/LEDataInputStream.java | 270 ++++----- .../keepass/stream/LEDataOutputStream.java | 204 +++---- .../keepass/stream/NullOutputStream.java | 40 +- .../stream/RandomFileOutputStream.java | 66 +-- .../kunzisoft/keepass/utils/EmptyUtils.java | 30 +- .../kunzisoft/keepass/utils/Interaction.java | 42 +- .../kunzisoft/keepass/utils/SprEngineV4.java | 356 ++++++------ .../com/kunzisoft/keepass/utils/Types.java | 228 ++++---- .../com/kunzisoft/keepass/utils/Util.java | 50 +- .../com/kunzisoft/keepass/utils/UuidUtil.java | 68 +-- 58 files changed, 3394 insertions(+), 3392 deletions(-) diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/AccentTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/AccentTest.java index 7aed9ad77..20b1a5d83 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/AccentTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/AccentTest.java @@ -1,22 +1,22 @@ /* -* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. -* -* This file is part of KeePass DX. -* -* KeePass DX is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* KeePass DX is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with KeePass DX. If not, see . -* -*/ + * 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; @@ -24,19 +24,21 @@ 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); - } - } + 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/OutputTests.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/OutputTests.java index bf93721fc..dde340c2b 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/OutputTests.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/OutputTests.java @@ -1,6 +1,6 @@ /* * 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 @@ -26,10 +26,10 @@ import android.test.suitebuilder.TestSuiteBuilder; public class OutputTests extends TestSuite { - public static Test suite() { + public static Test suite() { - return new TestSuiteBuilder(AllTests.class) - .includePackages("com.kunzisoft.keepass.tests.output") - .build(); - } + return new TestSuiteBuilder(AllTests.class) + .includePackages("com.kunzisoft.keepass.tests.output") + .build(); + } } diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwDateTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwDateTest.java index dacde9f56..492c4e5f2 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwDateTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwDateTest.java @@ -1,6 +1,6 @@ /* * 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 @@ -24,15 +24,15 @@ import junit.framework.TestCase; import com.kunzisoft.keepass.database.element.PwDate; public class PwDateTest extends TestCase { - public void testDate() { - PwDate jDate = new PwDate(System.currentTimeMillis()); - - PwDate intermediate = new PwDate(jDate); - - PwDate cDate = new PwDate(intermediate.getCDate(), 0); - - assertTrue("jDate and intermediate not equal", jDate.equals(intermediate)); - assertTrue("jDate and cDate not equal", cDate.equals(jDate)); - - } + public void testDate() { + PwDate jDate = new PwDate(System.currentTimeMillis()); + + PwDate intermediate = new PwDate(jDate); + + PwDate cDate = new PwDate(intermediate.getCDate(), 0); + + assertTrue("jDate and intermediate not equal", jDate.equals(intermediate)); + assertTrue("jDate and cDate not equal", cDate.equals(jDate)); + + } } diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV3.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV3.java index 8934da4de..8ec7691f4 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV3.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV3.java @@ -1,6 +1,6 @@ /* * 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 @@ -31,33 +31,33 @@ import com.kunzisoft.keepass.database.element.PwEntryV3; import com.kunzisoft.keepass.tests.database.TestData; public class PwEntryTestV3 extends AndroidTestCase { - PwEntryV3 mPE; - - @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); - } + 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 index 1644ba56f..ab1b7737f 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV4.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwEntryTestV4.java @@ -1,6 +1,6 @@ /* * 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 @@ -22,7 +22,7 @@ package com.kunzisoft.keepass.tests; import junit.framework.TestCase; public class PwEntryTestV4 extends TestCase { - public void testAssign() { + public void testAssign() { /* TODO Test PwEntryV4 entry = new PwEntryV4(); @@ -53,7 +53,7 @@ public class PwEntryTestV4 extends TestCase { /* 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 index 692d12952..097b2f294 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwGroupTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwGroupTest.java @@ -1,6 +1,6 @@ /* * 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 @@ -27,18 +27,18 @@ 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")); - } + 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 index bdec9c505..1a4ae6e1a 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/TestUtil.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/TestUtil.java @@ -1,6 +1,6 @@ /* * 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 @@ -33,29 +33,29 @@ import com.kunzisoft.keepass.utils.EmptyUtils; import com.kunzisoft.keepass.utils.UriUtil; public class TestUtil { - private static final File sdcard = Environment.getExternalStorageDirectory(); + private static final File sdcard = Environment.getExternalStorageDirectory(); - public static void extractKey(Context ctx, String asset, String target) throws Exception { - - 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 void extractKey(Context ctx, String asset, String target) throws Exception { - } + InputStream key = ctx.getAssets().open(asset, AssetManager.ACCESS_STREAMING); - public static String getSdPath(String filename) { - File file = new File(sdcard, filename); - return file.getAbsolutePath(); - } + 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 index d78d39932..aef346519 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.java @@ -1,6 +1,6 @@ /* * 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 @@ -35,177 +35,177 @@ 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 testReadWriteLongZero() { + testReadWriteLong((byte) 0); + } - } - - 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); + public void testReadWriteLongMax() { + testReadWriteLong(Byte.MAX_VALUE); + } - 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 testReadWriteLongMin() { + testReadWriteLong(Byte.MIN_VALUE); + } - } + public void testReadWriteLongRnd() { + Random rnd = new Random(); + byte[] buf = new byte[1]; + rnd.nextBytes(buf); - 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.writeTime(expected.getTime(), cal); - Calendar actual = Calendar.getInstance(); - actual.setTime(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)); - } - - 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); - } + testReadWriteLong(buf[0]); + } - public void testULongMax() throws Exception { - byte[] ulongBytes = new byte[8]; - for (int i = 0; i < ulongBytes.length; i++) { - ulongBytes[i] = -1; - } + private void testReadWriteLong(byte value) { + byte[] orig = new byte[8]; + byte[] dest = new byte[8]; - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - LEDataOutputStream leos = new LEDataOutputStream(bos); - leos.writeLong(Types.ULONG_MAX_VALUE); - leos.close(); + setArray(orig, value, 0, 8); - byte[] uLongMax = bos.toByteArray(); + long one = LEDataInputStream.readLong(orig, 0); + LEDataOutputStream.writeLong(one, dest, 0); - assertArrayEquals(ulongBytes, uLongMax); - } + 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.writeTime(expected.getTime(), cal); + Calendar actual = Calendar.getInstance(); + actual.setTime(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)); + } + + 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/crypto/AESTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/AESTest.java index db147ee45..ab91076bc 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/AESTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/AESTest.java @@ -1,22 +1,22 @@ /* -* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. -* -* This file is part of KeePass DX. -* -* KeePass DX is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* KeePass DX is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with KeePass DX. If not, see . -* -*/ + * 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; @@ -38,46 +38,46 @@ 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.getInstance("AES/CBC/PKCS5Padding", true); - android.init(Cipher.ENCRYPT_MODE, key, iv); - byte[] outAndroid = android.doFinal(input, 0, dataSize); - - Cipher nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding"); - nat.init(Cipher.ENCRYPT_MODE, key, iv); - byte[] outNative = nat.doFinal(input, 0, dataSize); - - assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative); - } - - + + 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.getInstance("AES/CBC/PKCS5Padding", true); + android.init(Cipher.ENCRYPT_MODE, key, iv); + byte[] outAndroid = android.doFinal(input, 0, dataSize); + + Cipher nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding"); + nat.init(Cipher.ENCRYPT_MODE, key, iv); + byte[] outNative = nat.doFinal(input, 0, dataSize); + + 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 index 4adbdbcdc..2dd634a11 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/CipherTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/CipherTest.java @@ -1,22 +1,22 @@ /* -* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. -* -* This file is part of KeePass DX. -* -* KeePass DX is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* KeePass DX is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with KeePass DX. If not, see . -* -*/ + * 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; @@ -44,57 +44,57 @@ import com.kunzisoft.keepass.stream.BetterCipherInputStream; import com.kunzisoft.keepass.stream.LEDataInputStream; public class CipherTest extends TestCase { - private Random rand = new Random(); - - 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.getInstance(AesEngine.CIPHER_UUID); - Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv); - Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv); + private Random rand = new Random(); - byte[] secrettext = encrypt.doFinal(plaintext); - byte[] decrypttext = decrypt.doFinal(secrettext); - - assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext); - } + public void testCipherFactory() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + byte[] key = new byte[32]; + byte[] iv = new byte[16]; - 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); + byte[] plaintext = new byte[1024]; - CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID); - Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv); - Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv); + rand.nextBytes(key); + rand.nextBytes(iv); + rand.nextBytes(plaintext); - 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); - } + CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID); + Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv); + Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv); + + byte[] secrettext = encrypt.doFinal(plaintext); + byte[] decrypttext = decrypt.doFinal(secrettext); + + 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.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/FinalKeyTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/FinalKeyTest.java index ba9bc8da4..2c36c9bac 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/FinalKeyTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/crypto/FinalKeyTest.java @@ -1,22 +1,22 @@ /* -* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. -* -* This file is part of KeePass DX. -* -* KeePass DX is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* KeePass DX is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with KeePass DX. If not, see . -* -*/ + * 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; @@ -30,37 +30,37 @@ import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey; import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey; public class FinalKeyTest extends TestCase { - private Random mRand; - - @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); - - } + 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/database/DeleteEntry.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/DeleteEntry.java index 15a46383c..ed01c2334 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/DeleteEntry.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/DeleteEntry.java @@ -1,6 +1,6 @@ /* * 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 @@ -26,15 +26,15 @@ import com.kunzisoft.keepass.database.element.PwDatabaseV3; import com.kunzisoft.keepass.database.element.PwEntryV3; public class DeleteEntry extends AndroidTestCase { - private static final String GROUP1_NAME = "Group1"; - private static final String ENTRY1_NAME = "Test1"; - private static final String ENTRY2_NAME = "Test2"; - private static final String KEYFILE = ""; - private static final String PASSWORD = "12345"; - private static final String ASSET = "delete.kdb"; - private static final String FILENAME = "/sdcard/delete.kdb"; - - public void testDelete() { + 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; @@ -76,9 +76,9 @@ public class DeleteEntry extends AndroidTestCase { assertNull("Group 1 was not removed.", group1); */ - } - - private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) { + } + + private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) { /* TODO test List entries = pm.getEntries(); @@ -89,11 +89,11 @@ public class DeleteEntry extends AndroidTestCase { } } */ - return null; - - } - - private GroupVersioned getGroup(PwDatabase pm, String name) { + return null; + + } + + private GroupVersioned getGroup(PwDatabase pm, String name) { /* List groups = pm.getGroups(); for ( int i = 0; i < groups.size(); i++ ) { @@ -103,9 +103,9 @@ public class DeleteEntry extends AndroidTestCase { } } */ - - return null; - } - + + 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 index c8b8f7f0c..fa009c76f 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/EntryV4.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/EntryV4.java @@ -1,6 +1,6 @@ /* * 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 @@ -26,7 +26,7 @@ import junit.framework.TestCase; public class EntryV4 extends TestCase { - public void testBackup() { + public void testBackup() { /* PwDatabaseV4 db = new PwDatabaseV4(); @@ -51,6 +51,6 @@ public class EntryV4 extends TestCase { assertEquals("Title2", backup.getTitle()); assertEquals("User2", backup.getUsername()); */ - } + } } 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 index fc897cf0a..46121d9db 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb3.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb3.java @@ -1,6 +1,6 @@ /* * 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 @@ -22,8 +22,8 @@ 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 { + + private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception { /* Context ctx = getContext(); @@ -40,14 +40,14 @@ public class Kdb3 extends AndroidTestCase { is.close(); */ - } - - public void testXMLKeyFile() throws Exception { - testKeyfile("kdb_with_xml_keyfile.kdb", "keyfile.key", "12345"); - } - - public void testBinary64KeyFile() throws Exception { - testKeyfile("binary-key.kdb", "binary.key", "12345"); - } + } + + public void testXMLKeyFile() throws Exception { + testKeyfile("kdb_with_xml_keyfile.kdb", "keyfile.key", "12345"); + } + + public void testBinary64KeyFile() throws Exception { + testKeyfile("binary-key.kdb", "binary.key", "12345"); + } } 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 index 3f41d341f..6c1105909 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb3Twofish.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb3Twofish.java @@ -1,6 +1,6 @@ /* * 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 @@ -22,7 +22,7 @@ package com.kunzisoft.keepass.tests.database; import android.test.AndroidTestCase; public class Kdb3Twofish extends AndroidTestCase { - public void testReadTwofish() throws Exception { + public void testReadTwofish() throws Exception { /* Context ctx = getContext(); @@ -37,5 +37,5 @@ public class Kdb3Twofish extends AndroidTestCase { is.close(); */ - } + } } 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 index 6dc7c0dcf..99ca1a9d1 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb4Header.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/Kdb4Header.java @@ -1,6 +1,6 @@ /* * 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 @@ -26,11 +26,11 @@ import android.test.AndroidTestCase; import java.io.InputStream; public class Kdb4Header extends AndroidTestCase { - public void testReadHeader() throws Exception { - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); + public void testReadHeader() throws Exception { + Context ctx = getContext(); + + AssetManager am = ctx.getAssets(); + InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); /* TODO Test @@ -45,5 +45,5 @@ public class Kdb4Header extends AndroidTestCase { is.close(); */ - } + } } 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 index 738319a0a..8cf081d38 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/SprEngineTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/SprEngineTest.java @@ -1,6 +1,6 @@ /* * 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 @@ -35,17 +35,17 @@ import java.util.UUID; import biz.source_code.base64Coder.Base64Coder; public class SprEngineTest extends AndroidTestCase { - private PwDatabaseV4 db; - private SprEngineV4 spr; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - Context ctx = getContext(); - - AssetManager am = ctx.getAssets(); - InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); + 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 @@ -56,12 +56,12 @@ public class SprEngineTest extends AndroidTestCase { spr = new SprEngineV4(); */ - } - - private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}"; - private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw=="; - private final String RESULT = "Password"; - public void testRefReplace() { + } + + private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}"; + private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw=="; + private final String RESULT = "Password"; + public void testRefReplace() { /* TODO TEST UUID entryUUID = decodeUUID(ENCODE_UUID); @@ -71,16 +71,16 @@ public class SprEngineTest extends AndroidTestCase { assertEquals(RESULT, spr.compile(REF, entry, db)); */ - - } - - private UUID decodeUUID(String encoded) { - if (encoded == null || encoded.length() == 0 ) { - return PwDatabase.UUID_ZERO; - } - - byte[] buf = Base64Coder.decode(encoded); - return Types.bytestoUUID(buf); - } + + } + + private UUID decodeUUID(String encoded) { + if (encoded == null || encoded.length() == 0 ) { + return PwDatabase.UUID_ZERO; + } + + byte[] buf = Base64Coder.decode(encoded); + return Types.bytestoUUID(buf); + } } 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 index 98cc21f2d..0d4dee7e6 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/TestData.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/database/TestData.java @@ -1,6 +1,6 @@ /* * 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 @@ -22,11 +22,11 @@ package com.kunzisoft.keepass.tests.database; import com.kunzisoft.keepass.database.element.Database; public class TestData { - private static final String TEST1_KEYFILE = ""; - private static final String TEST1_KDB = "test1.kdb"; - private static final String TEST1_PASSWORD = "12345"; + private static final String TEST1_KEYFILE = ""; + private static final String TEST1_KDB = "test1.kdb"; + private static final String TEST1_PASSWORD = "12345"; - private static Database mDb1; + private static Database mDb1; /* 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 index 55dccb177..68b221322 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/search/SearchTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/search/SearchTest.java @@ -1,22 +1,22 @@ /* -* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. -* -* This file is part of KeePass DX. -* -* KeePass DX is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* KeePass DX is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with KeePass DX. If not, see . -* -*/ + * 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; @@ -28,43 +28,43 @@ 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(); - - } + + 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 index 6b6bc7f01..ed14b8552 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/HashedBlock.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/HashedBlock.java @@ -1,22 +1,22 @@ /* -* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. -* -* This file is part of KeePass DX. -* -* KeePass DX is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* KeePass DX is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with KeePass DX. If not, see . -* -*/ + * 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; @@ -34,77 +34,77 @@ 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); + private static Random rand = new Random(); - 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); - - - } + 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/utils/StringUtilTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/utils/StringUtilTest.java index 3ee6130c6..12151eadd 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/utils/StringUtilTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/utils/StringUtilTest.java @@ -1,6 +1,6 @@ /* * 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 @@ -30,28 +30,28 @@ public class StringUtilTest extends TestCase { private final String search = "BcDe"; private final String badSearch = "Ed"; - public void testIndexOfIgnoreCase1() { - assertEquals(1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH)); - } + public void testIndexOfIgnoreCase1() { + assertEquals(1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH)); + } - public void testIndexOfIgnoreCase2() { - assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH), 2); - } + public void testIndexOfIgnoreCase2() { + assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH), 2); + } - public void testIndexOfIgnoreCase3() { - assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH)); - } - - 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 testIndexOfIgnoreCase3() { + assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH)); + } - public void testReplaceAllIgnoresCase2() { - assertEquals(repText, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH)); - } + private final String repText = "AbCtestingaBc"; + private final String repSearch = "ABc"; + private final String repSearchBad = "CCCCCC"; + private final String repNew = "12345"; + private final String repResult = "12345testing12345"; + public void testReplaceAllIgnoresCase1() { + assertEquals(repResult, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH)); + } + + public void testReplaceAllIgnoresCase2() { + assertEquals(repText, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH)); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/app/App.java b/app/src/main/java/com/kunzisoft/keepass/app/App.java index 2ccb90e21..2d888d74d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/App.java +++ b/app/src/main/java/com/kunzisoft/keepass/app/App.java @@ -1,6 +1,6 @@ /* * 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 @@ -29,46 +29,46 @@ import com.kunzisoft.keepass.stylish.Stylish; import java.util.Calendar; public class App extends MultiDexApplication { - private static Database db = null; - private static Calendar calendar = null; - private static RecentFileHistory fileHistory = null; - - public static Database getDB() { - if ( db == null ) { - db = new Database(); - } - return db; - } + private static Database db = null; + private static Calendar calendar = null; + private static RecentFileHistory fileHistory = null; - public static RecentFileHistory getFileHistory() { - return fileHistory; - } - - public static void setDB(Database d) { - db = d; - } + public static Database getDB() { + if ( db == null ) { + db = new Database(); + } + return db; + } - public static Calendar getCalendar() { - if ( calendar == null ) { - calendar = Calendar.getInstance(); - } - return calendar; - } + public static RecentFileHistory getFileHistory() { + return fileHistory; + } - @Override - public void onCreate() { - super.onCreate(); + public static void setDB(Database d) { + db = d; + } - Stylish.init(this); - fileHistory = new RecentFileHistory(this); - PRNGFixes.apply(); - } + public static Calendar getCalendar() { + if ( calendar == null ) { + calendar = Calendar.getInstance(); + } + return calendar; + } - @Override - public void onTerminate() { - if ( db != null ) { - db.closeAndClear(getApplicationContext()); - } - super.onTerminate(); - } + @Override + public void onCreate() { + super.onCreate(); + + Stylish.init(this); + fileHistory = new RecentFileHistory(this); + PRNGFixes.apply(); + } + + @Override + public void onTerminate() { + if ( db != null ) { + db.closeAndClear(getApplicationContext()); + } + super.onTerminate(); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/backup/SettingsBackupAgent.java b/app/src/main/java/com/kunzisoft/keepass/backup/SettingsBackupAgent.java index 156936444..a770898f7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/backup/SettingsBackupAgent.java +++ b/app/src/main/java/com/kunzisoft/keepass/backup/SettingsBackupAgent.java @@ -1,6 +1,6 @@ /* * 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 @@ -25,15 +25,15 @@ import android.app.backup.SharedPreferencesBackupHelper; @SuppressLint("NewApi") public class SettingsBackupAgent extends BackupAgentHelper { - - private static final String PREFS_BACKUP_KEY = "prefs"; - - @Override - public void onCreate() { - String defaultPrefs = this.getPackageName() + "_preferences"; - - SharedPreferencesBackupHelper prefHelper = new SharedPreferencesBackupHelper(this, defaultPrefs); - addHelper(PREFS_BACKUP_KEY, prefHelper); - } + + private static final String PREFS_BACKUP_KEY = "prefs"; + + @Override + public void onCreate() { + String defaultPrefs = this.getPackageName() + "_preferences"; + + SharedPreferencesBackupHelper prefHelper = new SharedPreferencesBackupHelper(this, defaultPrefs); + addHelper(PREFS_BACKUP_KEY, prefHelper); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/AESProvider.java b/app/src/main/java/com/kunzisoft/keepass/crypto/AESProvider.java index 97b1af712..4ef047276 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/AESProvider.java +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/AESProvider.java @@ -1,6 +1,6 @@ /* * 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 @@ -23,14 +23,14 @@ import java.security.Provider; public final class AESProvider extends Provider { - /** - * - */ - private static final long serialVersionUID = -3846349284296062658L; + /** + * + */ + private static final long serialVersionUID = -3846349284296062658L; - public AESProvider() { - super("AESProvider", 1.0, ""); - put("Cipher.AES",NativeAESCipherSpi.class.getName()); - } + public AESProvider() { + super("AESProvider", 1.0, ""); + put("Cipher.AES",NativeAESCipherSpi.class.getName()); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/CipherFactory.java b/app/src/main/java/com/kunzisoft/keepass/crypto/CipherFactory.java index 40e82d867..8abdcbac8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/CipherFactory.java +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/CipherFactory.java @@ -1,6 +1,6 @@ /* * 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 @@ -38,58 +38,58 @@ import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; public class CipherFactory { - private static boolean blacklistInit = false; - private static boolean blacklisted; + private static boolean blacklistInit = false; + private static boolean blacklisted; - static { - Security.addProvider(new BouncyCastleProvider()); - } - - public static Cipher getInstance(String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException { - return getInstance(transformation, false); - } - - public static Cipher getInstance(String transformation, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException { - // Return the native AES if it is possible - if ( (!deviceBlacklisted()) && (!androidOverride) && hasNativeImplementation(transformation) && NativeLib.loaded() ) { - return Cipher.getInstance(transformation, new AESProvider()); - } else { + static { + Security.addProvider(new BouncyCastleProvider()); + } + + public static Cipher getInstance(String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException { + return getInstance(transformation, false); + } + + public static Cipher getInstance(String transformation, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException { + // Return the native AES if it is possible + if ( (!deviceBlacklisted()) && (!androidOverride) && hasNativeImplementation(transformation) && NativeLib.loaded() ) { + return Cipher.getInstance(transformation, new AESProvider()); + } else { return Cipher.getInstance(transformation); - } - } - - public static boolean deviceBlacklisted() { - if (!blacklistInit) { - blacklistInit = true; - - // The Acer Iconia A500 is special and seems to always crash in the native crypto libraries - blacklisted = Build.MODEL.equals("A500"); - } - return blacklisted; - } - - private static boolean hasNativeImplementation(String transformation) { - return transformation.equals("AES/CBC/PKCS5Padding"); - } + } + } + + public static boolean deviceBlacklisted() { + if (!blacklistInit) { + blacklistInit = true; + + // The Acer Iconia A500 is special and seems to always crash in the native crypto libraries + blacklisted = Build.MODEL.equals("A500"); + } + return blacklisted; + } + + private static boolean hasNativeImplementation(String transformation) { + return transformation.equals("AES/CBC/PKCS5Padding"); + } - /** Generate appropriate cipher based on KeePass 2.x UUID's - * @param uuid - * @return - * @throws NoSuchPaddingException - * @throws NoSuchAlgorithmException - * @throws InvalidAlgorithmParameterException - * @throws InvalidKeyException - */ - public static CipherEngine getInstance(UUID uuid) throws NoSuchAlgorithmException { - if ( uuid.equals(AesEngine.CIPHER_UUID) ) { - return new AesEngine(); - } else if ( uuid.equals(TwofishEngine.CIPHER_UUID) ) { - return new TwofishEngine(); - } else if ( uuid.equals(ChaCha20Engine.CIPHER_UUID)) { - return new ChaCha20Engine(); - } - - throw new NoSuchAlgorithmException("UUID unrecognized."); - } + /** Generate appropriate cipher based on KeePass 2.x UUID's + * @param uuid + * @return + * @throws NoSuchPaddingException + * @throws NoSuchAlgorithmException + * @throws InvalidAlgorithmParameterException + * @throws InvalidKeyException + */ + public static CipherEngine getInstance(UUID uuid) throws NoSuchAlgorithmException { + if ( uuid.equals(AesEngine.CIPHER_UUID) ) { + return new AesEngine(); + } else if ( uuid.equals(TwofishEngine.CIPHER_UUID) ) { + return new TwofishEngine(); + } else if ( uuid.equals(ChaCha20Engine.CIPHER_UUID)) { + return new ChaCha20Engine(); + } + + throw new NoSuchAlgorithmException("UUID unrecognized."); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/NativeAESCipherSpi.java b/app/src/main/java/com/kunzisoft/keepass/crypto/NativeAESCipherSpi.java index e24550706..15d09bdc4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/NativeAESCipherSpi.java +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/NativeAESCipherSpi.java @@ -1,6 +1,6 @@ /* * 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 @@ -44,284 +44,284 @@ import javax.crypto.spec.IvParameterSpec; public class NativeAESCipherSpi extends CipherSpi { - private static final String TAG = NativeAESCipherSpi.class.getName(); + private static final String TAG = NativeAESCipherSpi.class.getName(); - private static boolean mIsStaticInit = false; - private static HashMap, Long> mCleanup = new HashMap, Long>(); - private static ReferenceQueue mQueue = new ReferenceQueue(); - - private final int AES_BLOCK_SIZE = 16; - private byte[] mIV; - - private boolean mIsInited = false; - private boolean mEncrypting = false; - private long mCtxPtr; - - private boolean mPadding = false; - - private static void staticInit() { - mIsStaticInit = true; - - // Start the cipher context cleanup thread to run forever - (new Thread(new Cleanup())).start(); - } - - private static void addToCleanupQueue(NativeAESCipherSpi ref, long ptr) { - Log.d(TAG, "queued cipher context: " + ptr); - mCleanup.put(new PhantomReference(ref, mQueue), ptr); - } - - /** Work with the garbage collector to clean up openssl memory when the cipher - * context is garbage collected. - * @author bpellin - * - */ - private static class Cleanup implements Runnable { + private static boolean mIsStaticInit = false; + private static HashMap, Long> mCleanup = new HashMap, Long>(); + private static ReferenceQueue mQueue = new ReferenceQueue(); - public void run() { - while (true) { - try { - Reference ref = mQueue.remove(); - - long ctx = mCleanup.remove(ref); - nCleanup(ctx); - Log.d(TAG, "Cleaned up cipher context: " + ctx); - - } catch (InterruptedException e) { - // Do nothing, but resume looping if mQueue.remove is interrupted - } - } - } - - } - - private static native void nCleanup(long ctxPtr); + private final int AES_BLOCK_SIZE = 16; + private byte[] mIV; - public NativeAESCipherSpi() { - if ( ! mIsStaticInit ) { - staticInit(); - } - } - - @Override - protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) - throws IllegalBlockSizeException, BadPaddingException { - int maxSize = engineGetOutputSize(inputLen); - byte[] output = new byte[maxSize]; - - int finalSize; - - try { - finalSize = doFinal(input, inputOffset, inputLen, output, 0); - } catch (ShortBufferException e) { - // This shouldn't be possible rethrow as RuntimeException - throw new RuntimeException("Short buffer exception shouldn't be possible from here."); - } - - if ( maxSize == finalSize ) { - return output; - } else { - // TODO: Special doFinal to avoid this copy - byte[] exact = new byte[finalSize]; - System.arraycopy(output, 0, exact, 0, finalSize); - return exact; - } - } + private boolean mIsInited = false; + private boolean mEncrypting = false; + private long mCtxPtr; - @Override - protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, - byte[] output, int outputOffset) throws ShortBufferException, - IllegalBlockSizeException, BadPaddingException { - - int result = doFinal(input, inputOffset, inputLen, output, outputOffset); - - if ( result == -1 ) { - throw new ShortBufferException(); - } - - return result; - } - - private int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) - throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { - - int outputSize = engineGetOutputSize(inputLen); - - int updateAmt; - if (input != null && inputLen > 0) { - updateAmt = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize); - } else { - updateAmt = 0; - } - - int finalAmt = nFinal(mCtxPtr, mPadding, output, outputOffset + updateAmt, outputSize - updateAmt); - - int out = updateAmt + finalAmt; - - - return out; - } - - private native int nFinal(long ctxPtr, boolean usePadding, byte[] output, int outputOffest, int outputSize) - throws ShortBufferException, IllegalBlockSizeException, BadPaddingException; + private boolean mPadding = false; - @Override - protected int engineGetBlockSize() { - return AES_BLOCK_SIZE; - } + private static void staticInit() { + mIsStaticInit = true; - @Override - protected byte[] engineGetIV() { - byte[] copyIV = new byte[0]; - if (mIV != null) { - int lengthIV = mIV.length; - copyIV = new byte[lengthIV]; - System.arraycopy(mIV, 0, copyIV, 0, lengthIV); - } - return copyIV; - } + // Start the cipher context cleanup thread to run forever + (new Thread(new Cleanup())).start(); + } - @Override - protected int engineGetOutputSize(int inputLen) { - return inputLen + nGetCacheSize(mCtxPtr) + AES_BLOCK_SIZE; - } - - private native int nGetCacheSize(long ctxPtr); + private static void addToCleanupQueue(NativeAESCipherSpi ref, long ptr) { + Log.d(TAG, "queued cipher context: " + ptr); + mCleanup.put(new PhantomReference(ref, mQueue), ptr); + } - @Override - protected AlgorithmParameters engineGetParameters() { - // TODO Auto-generated method stub - return null; - } + /** Work with the garbage collector to clean up openssl memory when the cipher + * context is garbage collected. + * @author bpellin + * + */ + private static class Cleanup implements Runnable { - @Override - protected void engineInit(int opmode, Key key, SecureRandom random) - throws InvalidKeyException { + public void run() { + while (true) { + try { + Reference ref = mQueue.remove(); - byte[] ivArray = new byte[16]; - random.nextBytes(ivArray); - - init(opmode, key, new IvParameterSpec(ivArray)); - } + long ctx = mCleanup.remove(ref); + nCleanup(ctx); + Log.d(TAG, "Cleaned up cipher context: " + ctx); - @Override - protected void engineInit(int opmode, Key key, - AlgorithmParameterSpec params, SecureRandom random) - throws InvalidKeyException, InvalidAlgorithmParameterException { - - IvParameterSpec ivparam; - - if ( params instanceof IvParameterSpec ) { - ivparam = (IvParameterSpec) params; - } else { - throw new InvalidAlgorithmParameterException("params must be an IvParameterSpec."); - } - - init(opmode, key, ivparam); - } - + } catch (InterruptedException e) { + // Do nothing, but resume looping if mQueue.remove is interrupted + } + } + } - @Override - protected void engineInit(int opmode, Key key, AlgorithmParameters params, - SecureRandom random) throws InvalidKeyException, - InvalidAlgorithmParameterException { - - try { - engineInit(opmode, key, params.getParameterSpec(AlgorithmParameterSpec.class), random); - } catch (InvalidParameterSpecException e) { - throw new InvalidAlgorithmParameterException(e); - } + } - } + private static native void nCleanup(long ctxPtr); - private void init(int opmode, Key key, IvParameterSpec params) { - if ( mIsInited ) { - // Do not allow multiple inits - assert(true); - throw new RuntimeException("Don't allow multiple inits"); - } else { - NativeLib.init(); - mIsInited = true; - } - - mIV = params.getIV(); - mEncrypting = opmode == Cipher.ENCRYPT_MODE; - mCtxPtr = nInit(mEncrypting, key.getEncoded(), mIV); - addToCleanupQueue(this, mCtxPtr); - } - - private native long nInit(boolean encrypting, byte[] key, byte[] iv); - - @Override - protected void engineSetMode(String mode) throws NoSuchAlgorithmException { - if ( ! mode.equals("CBC") ) { - throw new NoSuchAlgorithmException("This only supports CBC mode"); - } - } + public NativeAESCipherSpi() { + if ( ! mIsStaticInit ) { + staticInit(); + } + } - @Override - protected void engineSetPadding(String padding) - throws NoSuchPaddingException { - - if ( ! mIsInited ) { - NativeLib.init(); - } - - if ( padding.length() == 0 ) { - return; - } + @Override + protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + int maxSize = engineGetOutputSize(inputLen); + byte[] output = new byte[maxSize]; - if ( ! padding.equals("PKCS5Padding") ) { - throw new NoSuchPaddingException("Only supports PKCS5Padding."); - } - - mPadding = true; - - } - - @Override - protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { - int maxSize = engineGetOutputSize(inputLen); - byte output[] = new byte[maxSize]; - - int updateSize = update(input, inputOffset, inputLen, output, 0); - - if ( updateSize == maxSize ) { - return output; - } else { - // TODO: We could optimize update for this case to avoid this extra copy - byte[] exact = new byte[updateSize]; - System.arraycopy(output, 0, exact, 0, updateSize); - return exact; - } - - } + int finalSize; + + try { + finalSize = doFinal(input, inputOffset, inputLen, output, 0); + } catch (ShortBufferException e) { + // This shouldn't be possible rethrow as RuntimeException + throw new RuntimeException("Short buffer exception shouldn't be possible from here."); + } + + if ( maxSize == finalSize ) { + return output; + } else { + // TODO: Special doFinal to avoid this copy + byte[] exact = new byte[finalSize]; + System.arraycopy(output, 0, exact, 0, finalSize); + return exact; + } + } + + @Override + protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, + byte[] output, int outputOffset) throws ShortBufferException, + IllegalBlockSizeException, BadPaddingException { + + int result = doFinal(input, inputOffset, inputLen, output, outputOffset); + + if ( result == -1 ) { + throw new ShortBufferException(); + } + + return result; + } + + private int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) + throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + + int outputSize = engineGetOutputSize(inputLen); + + int updateAmt; + if (input != null && inputLen > 0) { + updateAmt = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize); + } else { + updateAmt = 0; + } + + int finalAmt = nFinal(mCtxPtr, mPadding, output, outputOffset + updateAmt, outputSize - updateAmt); + + int out = updateAmt + finalAmt; + + + return out; + } + + private native int nFinal(long ctxPtr, boolean usePadding, byte[] output, int outputOffest, int outputSize) + throws ShortBufferException, IllegalBlockSizeException, BadPaddingException; + + @Override + protected int engineGetBlockSize() { + return AES_BLOCK_SIZE; + } + + @Override + protected byte[] engineGetIV() { + byte[] copyIV = new byte[0]; + if (mIV != null) { + int lengthIV = mIV.length; + copyIV = new byte[lengthIV]; + System.arraycopy(mIV, 0, copyIV, 0, lengthIV); + } + return copyIV; + } + + @Override + protected int engineGetOutputSize(int inputLen) { + return inputLen + nGetCacheSize(mCtxPtr) + AES_BLOCK_SIZE; + } + + private native int nGetCacheSize(long ctxPtr); + + @Override + protected AlgorithmParameters engineGetParameters() { + // TODO Auto-generated method stub + return null; + } + + @Override + protected void engineInit(int opmode, Key key, SecureRandom random) + throws InvalidKeyException { + + byte[] ivArray = new byte[16]; + random.nextBytes(ivArray); + + init(opmode, key, new IvParameterSpec(ivArray)); + } + + @Override + protected void engineInit(int opmode, Key key, + AlgorithmParameterSpec params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + + IvParameterSpec ivparam; + + if ( params instanceof IvParameterSpec ) { + ivparam = (IvParameterSpec) params; + } else { + throw new InvalidAlgorithmParameterException("params must be an IvParameterSpec."); + } + + init(opmode, key, ivparam); + } + + + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameters params, + SecureRandom random) throws InvalidKeyException, + InvalidAlgorithmParameterException { + + try { + engineInit(opmode, key, params.getParameterSpec(AlgorithmParameterSpec.class), random); + } catch (InvalidParameterSpecException e) { + throw new InvalidAlgorithmParameterException(e); + } + + } + + private void init(int opmode, Key key, IvParameterSpec params) { + if ( mIsInited ) { + // Do not allow multiple inits + assert(true); + throw new RuntimeException("Don't allow multiple inits"); + } else { + NativeLib.init(); + mIsInited = true; + } + + mIV = params.getIV(); + mEncrypting = opmode == Cipher.ENCRYPT_MODE; + mCtxPtr = nInit(mEncrypting, key.getEncoded(), mIV); + addToCleanupQueue(this, mCtxPtr); + } + + private native long nInit(boolean encrypting, byte[] key, byte[] iv); + + @Override + protected void engineSetMode(String mode) throws NoSuchAlgorithmException { + if ( ! mode.equals("CBC") ) { + throw new NoSuchAlgorithmException("This only supports CBC mode"); + } + } + + @Override + protected void engineSetPadding(String padding) + throws NoSuchPaddingException { + + if ( ! mIsInited ) { + NativeLib.init(); + } + + if ( padding.length() == 0 ) { + return; + } + + if ( ! padding.equals("PKCS5Padding") ) { + throw new NoSuchPaddingException("Only supports PKCS5Padding."); + } + + mPadding = true; + + } + + @Override + protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + int maxSize = engineGetOutputSize(inputLen); + byte output[] = new byte[maxSize]; + + int updateSize = update(input, inputOffset, inputLen, output, 0); + + if ( updateSize == maxSize ) { + return output; + } else { + // TODO: We could optimize update for this case to avoid this extra copy + byte[] exact = new byte[updateSize]; + System.arraycopy(output, 0, exact, 0, updateSize); + return exact; + } + + } + + @Override + protected int engineUpdate(byte[] input, int inputOffset, int inputLen, + byte[] output, int outputOffset) throws ShortBufferException { + + int result = update(input, inputOffset, inputLen, output, outputOffset); + + if ( result == -1 ) { + throw new ShortBufferException("Insufficient buffer."); + } + + return result; + + } + + int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) { + int outputSize = engineGetOutputSize(inputLen); + + int out = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize); + + + return out; + + + } + + private native int nUpdate(long ctxPtr, byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset, int outputSize); - @Override - protected int engineUpdate(byte[] input, int inputOffset, int inputLen, - byte[] output, int outputOffset) throws ShortBufferException { - - int result = update(input, inputOffset, inputLen, output, outputOffset); - - if ( result == -1 ) { - throw new ShortBufferException("Insufficient buffer."); - } - - return result; - - } - - int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) { - int outputSize = engineGetOutputSize(inputLen); - - int out = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize); - - - return out; - - - } - - private native int nUpdate(long ctxPtr, byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset, int outputSize); - } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/NativeLib.java b/app/src/main/java/com/kunzisoft/keepass/crypto/NativeLib.java index 3f1b3f5ea..88a774220 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/NativeLib.java +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/NativeLib.java @@ -1,6 +1,6 @@ /* * 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 @@ -20,27 +20,27 @@ package com.kunzisoft.keepass.crypto; public class NativeLib { - private static boolean isLoaded = false; - private static boolean loadSuccess = false; - - public static boolean loaded() { - return init(); - } - - public static boolean init() { - if ( ! isLoaded ) { - try { - System.loadLibrary("final-key"); - System.loadLibrary("argon2"); - } catch ( UnsatisfiedLinkError e) { - return false; - } - isLoaded = true; - loadSuccess = true; - } - - return loadSuccess; - - } + private static boolean isLoaded = false; + private static boolean loadSuccess = false; + + public static boolean loaded() { + return init(); + } + + public static boolean init() { + if ( ! isLoaded ) { + try { + System.loadLibrary("final-key"); + System.loadLibrary("argon2"); + } catch ( UnsatisfiedLinkError e) { + return false; + } + isLoaded = true; + loadSuccess = true; + } + + return loadSuccess; + + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/PwStreamCipherFactory.java b/app/src/main/java/com/kunzisoft/keepass/crypto/PwStreamCipherFactory.java index d4a8f9866..2d5643dd1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/PwStreamCipherFactory.java +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/PwStreamCipherFactory.java @@ -1,6 +1,6 @@ /* * 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 @@ -26,48 +26,48 @@ import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.ParametersWithIV; public class PwStreamCipherFactory { - public static StreamCipher getInstance(CrsAlgorithm alg, byte[] key) { - if ( alg == CrsAlgorithm.Salsa20 ) { - return getSalsa20(key); - } else if (alg == CrsAlgorithm.ChaCha20) { - return getChaCha20(key); - } else { - return null; - } - } - - - private static final byte[] SALSA_IV = new byte[]{ (byte)0xE8, 0x30, 0x09, 0x4B, + public static StreamCipher getInstance(CrsAlgorithm alg, byte[] key) { + if ( alg == CrsAlgorithm.Salsa20 ) { + return getSalsa20(key); + } else if (alg == CrsAlgorithm.ChaCha20) { + return getChaCha20(key); + } else { + return null; + } + } + + + private static final byte[] SALSA_IV = new byte[]{ (byte)0xE8, 0x30, 0x09, 0x4B, (byte)0x97, 0x20, 0x5D, 0x2A }; - private static StreamCipher getSalsa20(byte[] key) { - // Build stream cipher key - byte[] key32 = CryptoUtil.hashSha256(key); + private static StreamCipher getSalsa20(byte[] key) { + // Build stream cipher key + byte[] key32 = CryptoUtil.hashSha256(key); - KeyParameter keyParam = new KeyParameter(key32); - ParametersWithIV ivParam = new ParametersWithIV(keyParam, SALSA_IV); + KeyParameter keyParam = new KeyParameter(key32); + ParametersWithIV ivParam = new ParametersWithIV(keyParam, SALSA_IV); - StreamCipher cipher = new Salsa20Engine(); - cipher.init(true, ivParam); + StreamCipher cipher = new Salsa20Engine(); + cipher.init(true, ivParam); - return cipher; - } + return cipher; + } - private static StreamCipher getChaCha20(byte[] key) { - // Build stream cipher key - byte[] hash = CryptoUtil.hashSha512(key); - byte[] key32 = new byte[32]; - byte[] iv = new byte[12]; + private static StreamCipher getChaCha20(byte[] key) { + // Build stream cipher key + byte[] hash = CryptoUtil.hashSha512(key); + byte[] key32 = new byte[32]; + byte[] iv = new byte[12]; - System.arraycopy(hash, 0, key32, 0, 32); + System.arraycopy(hash, 0, key32, 0, 32); System.arraycopy(hash, 32, iv, 0, 12); - KeyParameter keyParam = new KeyParameter(key32); - ParametersWithIV ivParam = new ParametersWithIV(keyParam, iv); + KeyParameter keyParam = new KeyParameter(key32); + ParametersWithIV ivParam = new ParametersWithIV(keyParam, iv); StreamCipher cipher = new ChaCha7539Engine(); - cipher.init(true, ivParam); - - return cipher; - } + cipher.init(true, ivParam); + + return cipher; + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/AndroidFinalKey.java b/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/AndroidFinalKey.java index 27b536379..b573f8629 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/AndroidFinalKey.java +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/AndroidFinalKey.java @@ -1,6 +1,6 @@ /* * 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 @@ -31,48 +31,48 @@ import javax.crypto.spec.SecretKeySpec; public class AndroidFinalKey extends FinalKey { - @Override - public byte[] transformMasterKey(byte[] pKeySeed, byte[] pKey, long rounds) throws IOException { - Cipher cipher; - try { - cipher = Cipher.getInstance("AES/ECB/NoPadding"); - } catch (NoSuchAlgorithmException e) { - throw new IOException("NoSuchAlgorithm: " + e.getMessage()); - } catch (NoSuchPaddingException e) { - throw new IOException("NoSuchPadding: " + e.getMessage()); - } + @Override + public byte[] transformMasterKey(byte[] pKeySeed, byte[] pKey, long rounds) throws IOException { + Cipher cipher; + try { + cipher = Cipher.getInstance("AES/ECB/NoPadding"); + } catch (NoSuchAlgorithmException e) { + throw new IOException("NoSuchAlgorithm: " + e.getMessage()); + } catch (NoSuchPaddingException e) { + throw new IOException("NoSuchPadding: " + e.getMessage()); + } - try { - cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(pKeySeed, "AES")); - } catch (InvalidKeyException e) { - throw new IOException("InvalidPasswordException: " + e.getMessage()); - } + try { + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(pKeySeed, "AES")); + } catch (InvalidKeyException e) { + throw new IOException("InvalidPasswordException: " + e.getMessage()); + } - // Encrypt key rounds times - byte[] newKey = new byte[pKey.length]; - System.arraycopy(pKey, 0, newKey, 0, pKey.length); - byte[] destKey = new byte[pKey.length]; - for (int i = 0; i < rounds; i++) { - try { - cipher.update(newKey, 0, newKey.length, destKey, 0); - System.arraycopy(destKey, 0, newKey, 0, newKey.length); + // Encrypt key rounds times + byte[] newKey = new byte[pKey.length]; + System.arraycopy(pKey, 0, newKey, 0, pKey.length); + byte[] destKey = new byte[pKey.length]; + for (int i = 0; i < rounds; i++) { + try { + cipher.update(newKey, 0, newKey.length, destKey, 0); + System.arraycopy(destKey, 0, newKey, 0, newKey.length); - } catch (ShortBufferException e) { - throw new IOException("Short buffer: " + e.getMessage()); - } - } + } catch (ShortBufferException e) { + throw new IOException("Short buffer: " + e.getMessage()); + } + } - // Hash the key - MessageDigest md = null; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - assert true; - throw new IOException("SHA-256 not implemented here: " + e.getMessage()); - } + // Hash the key + MessageDigest md = null; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + assert true; + throw new IOException("SHA-256 not implemented here: " + e.getMessage()); + } - md.update(newKey); - return md.digest(); - } + md.update(newKey); + return md.digest(); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/FinalKey.java b/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/FinalKey.java index cded04667..9495a6ce4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/FinalKey.java +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/FinalKey.java @@ -1,6 +1,6 @@ /* * 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 @@ -22,5 +22,5 @@ package com.kunzisoft.keepass.crypto.finalkey; import java.io.IOException; public abstract class FinalKey { - public abstract byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException; + public abstract byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException; } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/FinalKeyFactory.java b/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/FinalKeyFactory.java index 429dd21e1..851e29a52 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/FinalKeyFactory.java +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/FinalKeyFactory.java @@ -1,6 +1,6 @@ /* * 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 @@ -22,17 +22,17 @@ package com.kunzisoft.keepass.crypto.finalkey; import com.kunzisoft.keepass.crypto.CipherFactory; public class FinalKeyFactory { - public static FinalKey createFinalKey() { - return createFinalKey(false); - } - - public static FinalKey createFinalKey(boolean androidOverride) { - // Prefer the native final key implementation - if ( !CipherFactory.deviceBlacklisted() && !androidOverride && NativeFinalKey.availble() ) { - return new NativeFinalKey(); - } else { - // Fall back on the android crypto implementation - return new AndroidFinalKey(); - } - } + public static FinalKey createFinalKey() { + return createFinalKey(false); + } + + public static FinalKey createFinalKey(boolean androidOverride) { + // Prefer the native final key implementation + if ( !CipherFactory.deviceBlacklisted() && !androidOverride && NativeFinalKey.availble() ) { + return new NativeFinalKey(); + } else { + // Fall back on the android crypto implementation + return new AndroidFinalKey(); + } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/NativeFinalKey.java b/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/NativeFinalKey.java index 5e0900d92..66fae82b7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/NativeFinalKey.java +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/finalkey/NativeFinalKey.java @@ -1,6 +1,6 @@ /* * 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 @@ -25,22 +25,22 @@ import java.io.IOException; public class NativeFinalKey extends FinalKey { - - public static boolean availble() { - return NativeLib.init(); - } - @Override - public byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException { - NativeLib.init(); - - return nTransformMasterKey(seed, key, rounds); + public static boolean availble() { + return NativeLib.init(); + } - } - - private static native byte[] nTransformMasterKey(byte[] seed, byte[] key, long rounds); + @Override + public byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException { + NativeLib.init(); - // For testing + return nTransformMasterKey(seed, key, rounds); + + } + + private static native byte[] nTransformMasterKey(byte[] seed, byte[] key, long rounds); + + // For testing /* public static byte[] reflect(byte[] key) { NativeLib.init(); @@ -50,6 +50,6 @@ public class NativeFinalKey extends FinalKey { private static native byte[] nativeReflect(byte[] key); */ - + } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.java b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.java index eef5ea7a9..952a041c7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.java +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.java @@ -1,6 +1,6 @@ /* * Copyright 2019 Jeremy Jamet / Kunzisoft. - * + * * This file is part of KeePass DX. * * KeePass DX is free software: you can redistribute it and/or modify @@ -39,126 +39,126 @@ import java.util.*; */ public class PwDatabaseV3 extends PwDatabase { - private static final int DEFAULT_ENCRYPTION_ROUNDS = 300; + private static final int DEFAULT_ENCRYPTION_ROUNDS = 300; - private int numKeyEncRounds; + private int numKeyEncRounds; - protected PwGroupV3 rootGroup; + protected PwGroupV3 rootGroup; public PwDatabaseV3() { algorithm = PwEncryptionAlgorithm.AESRijndael; numKeyEncRounds = DEFAULT_ENCRYPTION_ROUNDS; } - @Override - public String getVersion() { - return "KeePass 1"; - } + @Override + public String getVersion() { + return "KeePass 1"; + } - @Override - public List getAvailableEncryptionAlgorithms() { - List list = new ArrayList<>(); - list.add(PwEncryptionAlgorithm.AESRijndael); - return list; - } + @Override + public List getAvailableEncryptionAlgorithms() { + List list = new ArrayList<>(); + list.add(PwEncryptionAlgorithm.AESRijndael); + return list; + } - public List getRootGroups() { + public List getRootGroups() { List kids = new ArrayList<>(); - for (Map.Entry group : groupIndexes.entrySet()) { - if (group.getValue().getLevel() == 0) - kids.add(group.getValue()); - } - return kids; - } + for (Map.Entry group : groupIndexes.entrySet()) { + if (group.getValue().getLevel() == 0) + kids.add(group.getValue()); + } + return kids; + } - private void assignGroupsChildren(PwGroupV3 parent) { - int levelToCheck = parent.getLevel() + 1; - boolean startFromParentPosition = false; - for (PwGroupV3 groupToCheck: getGroupIndexes()) { - if (getRootGroup().getNodeId().equals(parent.getNodeId()) - || groupToCheck.getNodeId().equals(parent.getNodeId())) { - startFromParentPosition = true; - } - if (startFromParentPosition) { - if (groupToCheck.getLevel() < levelToCheck) - break; - else if (groupToCheck.getLevel() == levelToCheck) - parent.addChildGroup(groupToCheck); - } - } - } + private void assignGroupsChildren(PwGroupV3 parent) { + int levelToCheck = parent.getLevel() + 1; + boolean startFromParentPosition = false; + for (PwGroupV3 groupToCheck: getGroupIndexes()) { + if (getRootGroup().getNodeId().equals(parent.getNodeId()) + || groupToCheck.getNodeId().equals(parent.getNodeId())) { + startFromParentPosition = true; + } + if (startFromParentPosition) { + if (groupToCheck.getLevel() < levelToCheck) + break; + else if (groupToCheck.getLevel() == levelToCheck) + parent.addChildGroup(groupToCheck); + } + } + } - private void assignEntriesChildren(PwGroupV3 parent) { - for (PwEntryV3 entry : getEntryIndexes()) { - if (entry.getParent().getNodeId().equals(parent.getNodeId())) - parent.addChildEntry(entry); - } - } + private void assignEntriesChildren(PwGroupV3 parent) { + for (PwEntryV3 entry : getEntryIndexes()) { + if (entry.getParent().getNodeId().equals(parent.getNodeId())) + parent.addChildEntry(entry); + } + } - private void constructTreeFromIndex(PwGroupV3 currentGroup) { + private void constructTreeFromIndex(PwGroupV3 currentGroup) { - assignGroupsChildren(currentGroup); - assignEntriesChildren(currentGroup); + assignGroupsChildren(currentGroup); + assignEntriesChildren(currentGroup); - // set parent in child entries (normally useless but to be sure or to update parent metadata) - for (PwEntryV3 childEntry : currentGroup.getChildEntries()) { - childEntry.setParent(currentGroup); - } - // recursively construct child groups - for (PwGroupV3 childGroup : currentGroup.getChildGroups()) { - childGroup.setParent(currentGroup); - constructTreeFromIndex(childGroup); - } - } + // set parent in child entries (normally useless but to be sure or to update parent metadata) + for (PwEntryV3 childEntry : currentGroup.getChildEntries()) { + childEntry.setParent(currentGroup); + } + // recursively construct child groups + for (PwGroupV3 childGroup : currentGroup.getChildGroups()) { + childGroup.setParent(currentGroup); + constructTreeFromIndex(childGroup); + } + } - public void constructTreeFromIndex() { - constructTreeFromIndex(getRootGroup()); - } + public void constructTreeFromIndex() { + constructTreeFromIndex(getRootGroup()); + } - /** - * Generates an unused random tree id - * - * @return new tree id - */ - @Override - public PwNodeIdInt newGroupId() { - PwNodeIdInt newId; - do { - newId = new PwNodeIdInt(); - } while (isGroupIdUsed(newId)); + /** + * Generates an unused random tree id + * + * @return new tree id + */ + @Override + public PwNodeIdInt newGroupId() { + PwNodeIdInt newId; + do { + newId = new PwNodeIdInt(); + } while (isGroupIdUsed(newId)); - return newId; - } + return newId; + } - /** - * Generates an unused random tree id - * - * @return new tree id - */ - @Override - public PwNodeIdUUID newEntryId() { - PwNodeIdUUID newId; - do { - newId = new PwNodeIdUUID(); - } while (isEntryIdUsed(newId)); + /** + * Generates an unused random tree id + * + * @return new tree id + */ + @Override + public PwNodeIdUUID newEntryId() { + PwNodeIdUUID newId; + do { + newId = new PwNodeIdUUID(); + } while (isEntryIdUsed(newId)); - return newId; - } + return newId; + } - @Override - public byte[] getMasterKey(@Nullable String key, @Nullable InputStream keyInputStream) - throws InvalidKeyFileException, IOException { + @Override + public byte[] getMasterKey(@Nullable String key, @Nullable InputStream keyInputStream) + throws InvalidKeyFileException, IOException { - if (key != null && keyInputStream != null) { - return getCompositeKey(key, keyInputStream); - } else if (key != null) { // key.length() >= 0 - return getPasswordKey(key); - } else if (keyInputStream != null) { // key == null - return getFileKey(keyInputStream); - } else { - throw new IllegalArgumentException("Key cannot be empty."); - } - } + if (key != null && keyInputStream != null) { + return getCompositeKey(key, keyInputStream); + } else if (key != null) { // key.length() >= 0 + return getPasswordKey(key); + } else if (keyInputStream != null) { // key == null + return getFileKey(keyInputStream); + } else { + throw new IllegalArgumentException("Key cannot be empty."); + } + } /** * Encrypt the master key a few times to make brute-force key-search harder @@ -170,84 +170,84 @@ public class PwDatabaseV3 extends PwDatabase { return key.transformMasterKey(pKeySeed, pKey, rounds); } - public void makeFinalKey(byte[] masterSeed, byte[] masterSeed2, long numRounds) throws IOException { + public void makeFinalKey(byte[] masterSeed, byte[] masterSeed2, long numRounds) throws IOException { - // Write checksum Checksum - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IOException("SHA-256 not implemented here."); - } - NullOutputStream nos = new NullOutputStream(); - DigestOutputStream dos = new DigestOutputStream(nos, md); + // Write checksum Checksum + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 not implemented here."); + } + NullOutputStream nos = new NullOutputStream(); + DigestOutputStream dos = new DigestOutputStream(nos, md); - byte[] transformedMasterKey = transformMasterKey(masterSeed2, masterKey, numRounds); - dos.write(masterSeed); - dos.write(transformedMasterKey); + byte[] transformedMasterKey = transformMasterKey(masterSeed2, masterKey, numRounds); + dos.write(masterSeed); + dos.write(transformedMasterKey); - finalKey = md.digest(); - } + finalKey = md.digest(); + } - @Override - protected String getPasswordEncoding() { - return "ISO-8859-1"; - } - - @Override - protected byte[] loadXmlKeyFile(InputStream keyInputStream) { - return null; - } + @Override + protected String getPasswordEncoding() { + return "ISO-8859-1"; + } + + @Override + protected byte[] loadXmlKeyFile(InputStream keyInputStream) { + return null; + } - @Override - public long getNumberKeyEncryptionRounds() { - return numKeyEncRounds; - } + @Override + public long getNumberKeyEncryptionRounds() { + return numKeyEncRounds; + } - @Override - public void setNumberKeyEncryptionRounds(long rounds) throws NumberFormatException { - if (rounds > Integer.MAX_VALUE || rounds < Integer.MIN_VALUE) { - throw new NumberFormatException(); - } - numKeyEncRounds = (int) rounds; - } + @Override + public void setNumberKeyEncryptionRounds(long rounds) throws NumberFormatException { + if (rounds > Integer.MAX_VALUE || rounds < Integer.MIN_VALUE) { + throw new NumberFormatException(); + } + numKeyEncRounds = (int) rounds; + } - @Override - public PwGroupV3 createGroup() { - return new PwGroupV3(); - } + @Override + public PwGroupV3 createGroup() { + return new PwGroupV3(); + } - public void setRootGroup(PwGroupV3 rootGroup) { - this.rootGroup = rootGroup; - } + public void setRootGroup(PwGroupV3 rootGroup) { + this.rootGroup = rootGroup; + } - @Override - public PwGroupV3 getRootGroup() { - return rootGroup; - } + @Override + public PwGroupV3 getRootGroup() { + return rootGroup; + } - @Override - public PwEntryV3 createEntry() { - return new PwEntryV3(); - } + @Override + public PwEntryV3 createEntry() { + return new PwEntryV3(); + } - @Override - public boolean isBackup(PwGroupV3 group) { - while (group != null) { - if (group.getLevel() == 0 && group.getTitle().equalsIgnoreCase("Backup")) { - return true; - } - group = group.getParent(); - } - return false; - } + @Override + public boolean isBackup(PwGroupV3 group) { + while (group != null) { + if (group.getLevel() == 0 && group.getTitle().equalsIgnoreCase("Backup")) { + return true; + } + group = group.getParent(); + } + return false; + } - @Override - public boolean isGroupSearchable(PwGroupV3 group, boolean omitBackup) { - if (!super.isGroupSearchable(group, omitBackup)) { - return false; - } - return !(omitBackup && isBackup(group)); - } + @Override + public boolean isGroupSearchable(PwGroupV3 group, boolean omitBackup) { + if (!super.isGroupSearchable(group, omitBackup)) { + return false; + } + return !(omitBackup && isBackup(group)); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.java b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.java index 3fb4d2029..8e535faad 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.java +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.java @@ -1,6 +1,6 @@ /* * 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 @@ -37,11 +37,11 @@ import java.util.Date; */ public class PwDate implements Parcelable { - private static final int DATE_SIZE = 5; + private static final int DATE_SIZE = 5; private Date jDate = null; - private boolean jDateBuilt = false; - transient private byte[] cDate = null; + private boolean jDateBuilt = false; + transient private byte[] cDate = null; transient private boolean cDateBuilt = false; public static final Date NEVER_EXPIRE = getNeverExpire(); @@ -73,47 +73,47 @@ public class PwDate implements Parcelable { return cal.getTime(); } - - public PwDate(byte[] buf, int offset) { - cDate = new byte[DATE_SIZE]; - System.arraycopy(buf, offset, cDate, 0, DATE_SIZE); - cDateBuilt = true; - } - public PwDate(PwDate source) { - if (source.jDate != null) { - this.jDate = new Date(source.jDate.getTime()); - } - this.jDateBuilt = source.jDateBuilt; + public PwDate(byte[] buf, int offset) { + cDate = new byte[DATE_SIZE]; + System.arraycopy(buf, offset, cDate, 0, DATE_SIZE); + cDateBuilt = true; + } - if (source.cDate != null) { - int dateLength = source.cDate.length; - this.cDate = new byte[dateLength]; - System.arraycopy(source.cDate, 0, this.cDate, 0, dateLength); - } - this.cDateBuilt = source.cDateBuilt; - } + public PwDate(PwDate source) { + if (source.jDate != null) { + this.jDate = new Date(source.jDate.getTime()); + } + this.jDateBuilt = source.jDateBuilt; - public PwDate(Date date) { - jDate = new Date(date.getTime()); - jDateBuilt = true; - } - - public PwDate(long millis) { - jDate = new Date(millis); - jDateBuilt = true; - } - - public PwDate() { - jDate = new Date(); - jDateBuilt = true; - } + if (source.cDate != null) { + int dateLength = source.cDate.length; + this.cDate = new byte[dateLength]; + System.arraycopy(source.cDate, 0, this.cDate, 0, dateLength); + } + this.cDateBuilt = source.cDateBuilt; + } - protected PwDate(Parcel in) { - jDate = (Date) in.readSerializable(); - jDateBuilt = in.readByte() != 0; + public PwDate(Date date) { + jDate = new Date(date.getTime()); + jDateBuilt = true; + } + + public PwDate(long millis) { + jDate = new Date(millis); + jDateBuilt = true; + } + + public PwDate() { + jDate = new Date(); + jDateBuilt = true; + } + + protected PwDate(Parcel in) { + jDate = (Date) in.readSerializable(); + jDateBuilt = in.readByte() != 0; cDateBuilt = false; - } + } @Override public int describeContents() { @@ -121,146 +121,146 @@ public class PwDate implements Parcelable { } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(Parcel dest, int flags) { dest.writeSerializable(getDate()); dest.writeByte((byte) (jDateBuilt ? 1 : 0)); - } + } - public static final Creator CREATOR = new Creator() { - @Override - public PwDate createFromParcel(Parcel in) { - return new PwDate(in); - } + public static final Creator CREATOR = new Creator() { + @Override + public PwDate createFromParcel(Parcel in) { + return new PwDate(in); + } - @Override - public PwDate[] newArray(int size) { - return new PwDate[size]; - } - }; + @Override + public PwDate[] newArray(int size) { + return new PwDate[size]; + } + }; - public Date getDate() { - if ( ! jDateBuilt ) { - jDate = readTime(cDate, 0, App.getCalendar()); - jDateBuilt = true; - } - - return jDate; - } - - public byte[] getCDate() { - if ( ! cDateBuilt ) { - cDate = writeTime(jDate, App.getCalendar()); - cDateBuilt = true; - } - - return cDate; - } - - /** - * Unpack date from 5 byte format. The five bytes at 'offset' are unpacked - * to a java.util.Date instance. - */ - public static Date readTime(byte[] buf, int offset, Calendar time) { - int dw1 = Types.readUByte(buf, offset); - int dw2 = Types.readUByte(buf, offset + 1); - int dw3 = Types.readUByte(buf, offset + 2); - int dw4 = Types.readUByte(buf, offset + 3); - int dw5 = Types.readUByte(buf, offset + 4); + public Date getDate() { + if ( ! jDateBuilt ) { + jDate = readTime(cDate, 0, App.getCalendar()); + jDateBuilt = true; + } - // Unpack 5 byte structure to date and time - int year = (dw1 << 6) | (dw2 >> 2); - int month = ((dw2 & 0x00000003) << 2) | (dw3 >> 6); + return jDate; + } - int day = (dw3 >> 1) & 0x0000001F; - int hour = ((dw3 & 0x00000001) << 4) | (dw4 >> 4); - int minute = ((dw4 & 0x0000000F) << 2) | (dw5 >> 6); - int second = dw5 & 0x0000003F; + public byte[] getCDate() { + if ( ! cDateBuilt ) { + cDate = writeTime(jDate, App.getCalendar()); + cDateBuilt = true; + } - if (time == null) { - time = Calendar.getInstance(); - } - // File format is a 1 based month, java Calendar uses a zero based month - // File format is a 1 based day, java Calendar uses a 1 based day - time.set(year, month - 1, day, hour, minute, second); + return cDate; + } - return time.getTime(); + /** + * Unpack date from 5 byte format. The five bytes at 'offset' are unpacked + * to a java.util.Date instance. + */ + public static Date readTime(byte[] buf, int offset, Calendar time) { + int dw1 = Types.readUByte(buf, offset); + int dw2 = Types.readUByte(buf, offset + 1); + int dw3 = Types.readUByte(buf, offset + 2); + int dw4 = Types.readUByte(buf, offset + 3); + int dw5 = Types.readUByte(buf, offset + 4); - } + // Unpack 5 byte structure to date and time + int year = (dw1 << 6) | (dw2 >> 2); + int month = ((dw2 & 0x00000003) << 2) | (dw3 >> 6); - public static byte[] writeTime(Date date) { - return writeTime(date, null); - } - - public static byte[] writeTime(Date date, Calendar cal) { - if (date == null) { - return null; - } + int day = (dw3 >> 1) & 0x0000001F; + int hour = ((dw3 & 0x00000001) << 4) | (dw4 >> 4); + int minute = ((dw4 & 0x0000000F) << 2) | (dw5 >> 6); + int second = dw5 & 0x0000003F; - byte[] buf = new byte[5]; - if (cal == null) { - cal = Calendar.getInstance(); - } - cal.setTime(date); + if (time == null) { + time = Calendar.getInstance(); + } + // File format is a 1 based month, java Calendar uses a zero based month + // File format is a 1 based day, java Calendar uses a 1 based day + time.set(year, month - 1, day, hour, minute, second); - int year = cal.get(Calendar.YEAR); - // File format is a 1 based month, java Calendar uses a zero based month - int month = cal.get(Calendar.MONTH) + 1; - // File format is a 0 based day, java Calendar uses a 1 based day - int day = cal.get(Calendar.DAY_OF_MONTH) - 1; - int hour = cal.get(Calendar.HOUR_OF_DAY); - int minute = cal.get(Calendar.MINUTE); - int second = cal.get(Calendar.SECOND); + return time.getTime(); - buf[0] = Types.writeUByte(((year >> 6) & 0x0000003F)); - buf[1] = Types.writeUByte(((year & 0x0000003F) << 2) - | ((month >> 2) & 0x00000003)); - buf[2] = (byte) (((month & 0x00000003) << 6) - | ((day & 0x0000001F) << 1) | ((hour >> 4) & 0x00000001)); - buf[3] = (byte) (((hour & 0x0000000F) << 4) | ((minute >> 2) & 0x0000000F)); - buf[4] = (byte) (((minute & 0x00000003) << 6) | (second & 0x0000003F)); + } - return buf; - } + public static byte[] writeTime(Date date) { + return writeTime(date, null); + } - @Override - public boolean equals(Object o) { - if ( this == o ) { - return true; - } - if ( o == null ) { - return false; - } - if ( getClass() != o.getClass() ) { - return false; - } - - PwDate date = (PwDate) o; - if ( cDateBuilt && date.cDateBuilt ) { - return Arrays.equals(cDate, date.cDate); - } else if ( jDateBuilt && date.jDateBuilt ) { - return IsSameDate(jDate, date.jDate); - } else if ( cDateBuilt && date.jDateBuilt ) { - return Arrays.equals(date.getCDate(), cDate); - } else { - return IsSameDate(date.getDate(), jDate); - } - } + public static byte[] writeTime(Date date, Calendar cal) { + if (date == null) { + return null; + } - private static boolean IsSameDate(Date d1, Date d2) { - Calendar cal1 = Calendar.getInstance(); - cal1.setTime(d1); - cal1.set(Calendar.MILLISECOND, 0); - - Calendar cal2 = Calendar.getInstance(); - cal2.setTime(d2); - cal2.set(Calendar.MILLISECOND, 0); - - return (cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)) && - (cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH)) && - (cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH)) && - (cal1.get(Calendar.HOUR) == cal2.get(Calendar.HOUR)) && - (cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE)) && - (cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND)); - - } + byte[] buf = new byte[5]; + if (cal == null) { + cal = Calendar.getInstance(); + } + cal.setTime(date); + + int year = cal.get(Calendar.YEAR); + // File format is a 1 based month, java Calendar uses a zero based month + int month = cal.get(Calendar.MONTH) + 1; + // File format is a 0 based day, java Calendar uses a 1 based day + int day = cal.get(Calendar.DAY_OF_MONTH) - 1; + int hour = cal.get(Calendar.HOUR_OF_DAY); + int minute = cal.get(Calendar.MINUTE); + int second = cal.get(Calendar.SECOND); + + buf[0] = Types.writeUByte(((year >> 6) & 0x0000003F)); + buf[1] = Types.writeUByte(((year & 0x0000003F) << 2) + | ((month >> 2) & 0x00000003)); + buf[2] = (byte) (((month & 0x00000003) << 6) + | ((day & 0x0000001F) << 1) | ((hour >> 4) & 0x00000001)); + buf[3] = (byte) (((hour & 0x0000000F) << 4) | ((minute >> 2) & 0x0000000F)); + buf[4] = (byte) (((minute & 0x00000003) << 6) | (second & 0x0000003F)); + + return buf; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null ) { + return false; + } + if ( getClass() != o.getClass() ) { + return false; + } + + PwDate date = (PwDate) o; + if ( cDateBuilt && date.cDateBuilt ) { + return Arrays.equals(cDate, date.cDate); + } else if ( jDateBuilt && date.jDateBuilt ) { + return IsSameDate(jDate, date.jDate); + } else if ( cDateBuilt && date.jDateBuilt ) { + return Arrays.equals(date.getCDate(), cDate); + } else { + return IsSameDate(date.getDate(), jDate); + } + } + + private static boolean IsSameDate(Date d1, Date d2) { + Calendar cal1 = Calendar.getInstance(); + cal1.setTime(d1); + cal1.set(Calendar.MILLISECOND, 0); + + Calendar cal2 = Calendar.getInstance(); + cal2.setTime(d2); + cal2.set(Calendar.MILLISECOND, 0); + + return (cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)) && + (cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH)) && + (cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH)) && + (cal1.get(Calendar.HOUR) == cal2.get(Calendar.HOUR)) && + (cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE)) && + (cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND)); + + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java b/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java index 8cb52d41f..3f819093c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java @@ -1,6 +1,6 @@ /* * 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 @@ -45,16 +45,16 @@ import com.kunzisoft.keepass.stylish.StylishActivity; public class IconPickerDialogFragment extends DialogFragment { - public static final String KEY_ICON_STANDARD = "KEY_ICON_STANDARD"; + public static final String KEY_ICON_STANDARD = "KEY_ICON_STANDARD"; - private IconPickerListener iconPickerListener; - private IconPack iconPack; + private IconPickerListener iconPickerListener; + private IconPack iconPack; - public static void launch(StylishActivity activity) { + public static void launch(StylishActivity activity) { // Create an instance of the dialog fragment and show it IconPickerDialogFragment dialog = new IconPickerDialogFragment(); dialog.show(activity.getSupportFragmentManager(), "IconPickerDialogFragment"); - } + } @Override public void onAttach(Context context) { @@ -66,86 +66,86 @@ public class IconPickerDialogFragment extends DialogFragment { throw new ClassCastException(context.toString() + " must implement " + IconPickerListener.class.getName()); } - } + } - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - // Get the layout inflater - LayoutInflater inflater = getActivity().getLayoutInflater(); + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + // Get the layout inflater + LayoutInflater inflater = getActivity().getLayoutInflater(); - iconPack = IconPackChooser.getSelectedIconPack(getContext()); + iconPack = IconPackChooser.getSelectedIconPack(getContext()); - // Inflate and set the layout for the dialog - // Pass null as the parent view because its going in the dialog layout - View root = inflater.inflate(R.layout.icon_picker, null); - builder.setView(root); + // Inflate and set the layout for the dialog + // Pass null as the parent view because its going in the dialog layout + View root = inflater.inflate(R.layout.icon_picker, null); + builder.setView(root); - GridView currIconGridView = root.findViewById(R.id.IconGridView); - currIconGridView.setAdapter(new ImageAdapter(this.getContext())); + GridView currIconGridView = root.findViewById(R.id.IconGridView); + currIconGridView.setAdapter(new ImageAdapter(this.getContext())); - currIconGridView.setOnItemClickListener((parent, v, position, id) -> { + currIconGridView.setOnItemClickListener((parent, v, position, id) -> { Bundle bundle = new Bundle(); - bundle.putParcelable(KEY_ICON_STANDARD, new PwIconStandard(position)); - iconPickerListener.iconPicked(bundle); + bundle.putParcelable(KEY_ICON_STANDARD, new PwIconStandard(position)); + iconPickerListener.iconPicked(bundle); dismiss(); }); builder.setNegativeButton(R.string.cancel, (dialog, id) -> - IconPickerDialogFragment.this.getDialog().cancel()); + IconPickerDialogFragment.this.getDialog().cancel()); - return builder.create(); - } - - public class ImageAdapter extends BaseAdapter { - private Context context; + return builder.create(); + } - ImageAdapter(Context c) { - context = c; - } + public class ImageAdapter extends BaseAdapter { + private Context context; - public int getCount() { - /* Return number of KeePass icons */ - return iconPack.numberOfIcons(); - } - - public Object getItem(int position) { - return null; - } + ImageAdapter(Context c) { + context = c; + } - public long getItemId(int position) { - return 0; - } - - public View getView(int position, View convertView, ViewGroup parent) { - View currView; - if(convertView == null) { - LayoutInflater li = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + public int getCount() { + /* Return number of KeePass icons */ + return iconPack.numberOfIcons(); + } + + public Object getItem(int position) { + return null; + } + + public long getItemId(int position) { + return 0; + } + + public View getView(int position, View convertView, ViewGroup parent) { + View currView; + if(convertView == null) { + LayoutInflater li = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); assert li != null; currView = li.inflate(R.layout.icon, parent, false); - } - else { - currView = convertView; - } - ImageView iv = currView.findViewById(R.id.icon_image); - iv.setImageResource(iconPack.iconToResId(position)); + } + else { + currView = convertView; + } + ImageView iv = currView.findViewById(R.id.icon_image); + iv.setImageResource(iconPack.iconToResId(position)); - // Assign color if icons are tintable - if (iconPack.tintable()) { - // Retrieve the textColor to tint the icon - int[] attrs = {android.R.attr.textColor}; - assert getContext() != null; - TypedArray ta = getContext().getTheme().obtainStyledAttributes(attrs); - int iconColor = ta.getColor(0, Color.BLACK); + // Assign color if icons are tintable + if (iconPack.tintable()) { + // Retrieve the textColor to tint the icon + int[] attrs = {android.R.attr.textColor}; + assert getContext() != null; + TypedArray ta = getContext().getTheme().obtainStyledAttributes(attrs); + int iconColor = ta.getColor(0, Color.BLACK); ImageViewCompat.setImageTintList(iv, ColorStateList.valueOf(iconColor)); - } + } - return currView; - } - } + return currView; + } + } - public interface IconPickerListener { - void iconPicked(Bundle bundle); - } + public interface IconPickerListener { + void iconPicked(Bundle bundle); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/ReadOnlyDialog.java b/app/src/main/java/com/kunzisoft/keepass/dialogs/ReadOnlyDialog.java index a237cd052..014c38444 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/ReadOnlyDialog.java +++ b/app/src/main/java/com/kunzisoft/keepass/dialogs/ReadOnlyDialog.java @@ -1,6 +1,6 @@ /* * 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 @@ -25,14 +25,14 @@ import android.os.Build; import com.kunzisoft.keepass.R; public class ReadOnlyDialog extends WarningDialog { - - public ReadOnlyDialog(Context context) { - super(context, R.string.show_read_only_warning); - - warning = context.getString(R.string.read_only_warning); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - warning = warning.concat("\n\n").concat(context.getString(R.string.read_only_kitkat_warning)); - } - } + + public ReadOnlyDialog(Context context) { + super(context, R.string.show_read_only_warning); + + warning = context.getString(R.string.read_only_warning); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + warning = warning.concat("\n\n").concat(context.getString(R.string.read_only_kitkat_warning)); + } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/WarningDialog.java b/app/src/main/java/com/kunzisoft/keepass/dialogs/WarningDialog.java index 7511672fe..27dde376b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/WarningDialog.java +++ b/app/src/main/java/com/kunzisoft/keepass/dialogs/WarningDialog.java @@ -1,6 +1,6 @@ /* * 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 @@ -29,50 +29,50 @@ import android.preference.PreferenceManager; import com.kunzisoft.keepass.R; public class WarningDialog extends AlertDialog { - - protected String warning; - private int showKey; - public WarningDialog(Context context, int dontShowKey) { - super(context); - - this.showKey = dontShowKey; - } - - public WarningDialog(Context context, int warningKey, int dontShowKey) { - this(context, dontShowKey); + protected String warning; + private int showKey; - warning = context.getString(warningKey); - } + public WarningDialog(Context context, int dontShowKey) { + super(context); - @Override - protected void onCreate(Bundle savedInstanceState) { - Context ctx = getContext(); - setMessage(warning); - - setButton(AlertDialog.BUTTON1, ctx.getText(android.R.string.ok), new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dismiss(); - } - }); - - setButton(AlertDialog.BUTTON2, ctx.getText(R.string.beta_dontask), new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - Context ctx = getContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); - SharedPreferences.Editor edit = prefs.edit(); - edit.putBoolean(ctx.getString(showKey), false); - edit.commit(); - - dismiss(); - } - }); - - super.onCreate(savedInstanceState); - } + this.showKey = dontShowKey; + } + + public WarningDialog(Context context, int warningKey, int dontShowKey) { + this(context, dontShowKey); + + warning = context.getString(warningKey); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + Context ctx = getContext(); + setMessage(warning); + + setButton(AlertDialog.BUTTON1, ctx.getText(android.R.string.ok), new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + } + }); + + setButton(AlertDialog.BUTTON2, ctx.getText(R.string.beta_dontask), new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Context ctx = getContext(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); + SharedPreferences.Editor edit = prefs.edit(); + edit.putBoolean(ctx.getString(showKey), false); + edit.commit(); + + dismiss(); + } + }); + + super.onCreate(savedInstanceState); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/BrowserDialog.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/BrowserDialog.java index 4ded1e954..d62e1f23e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/BrowserDialog.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/BrowserDialog.java @@ -1,6 +1,6 @@ /* * 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 @@ -33,29 +33,29 @@ import com.kunzisoft.keepass.utils.Util; public class BrowserDialog extends DialogFragment { - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - // Get the layout inflater - LayoutInflater inflater = getActivity().getLayoutInflater(); - View root = inflater.inflate(R.layout.browser_install, null); - builder.setView(root) - .setNegativeButton(R.string.cancel, (dialog, id) -> { }); + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + // Get the layout inflater + LayoutInflater inflater = getActivity().getLayoutInflater(); + View root = inflater.inflate(R.layout.browser_install, null); + builder.setView(root) + .setNegativeButton(R.string.cancel, (dialog, id) -> { }); - Button market = root.findViewById(R.id.install_market); - market.setOnClickListener((view) -> { - Util.gotoUrl(getContext(), R.string.filemanager_play_store); - dismiss(); - }); + Button market = root.findViewById(R.id.install_market); + market.setOnClickListener((view) -> { + Util.gotoUrl(getContext(), R.string.filemanager_play_store); + dismiss(); + }); - Button web = root.findViewById(R.id.install_web); - web.setOnClickListener(view -> { + Button web = root.findViewById(R.id.install_web); + web.setOnClickListener(view -> { Util.gotoUrl(getContext(), R.string.filemanager_f_droid); dismiss(); }); - return builder.create(); - } + return builder.create(); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDbHelper.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDbHelper.java index 17d320578..2738173b6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDbHelper.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDbHelper.java @@ -1,6 +1,6 @@ /* * 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 @@ -34,221 +34,221 @@ import java.io.FileFilter; public class FileDbHelper { private static final String TAG = FileDbHelper.class.getName(); - - public static final String LAST_FILENAME = "lastFile"; - public static final String LAST_KEYFILE = "lastKey"; - - public static final String DATABASE_NAME = "keepassdroid"; // TODO Change db name - private static final String FILE_TABLE = "files"; - private static final int DATABASE_VERSION = 1; - - public static final int MAX_FILES = 5; - - public static final String KEY_FILE_ID = "_id"; - public static final String KEY_FILE_FILENAME = "fileName"; - public static final String KEY_FILE_KEYFILE = "keyFile"; - public static final String KEY_FILE_UPDATED = "updated"; - private static final String DATABASE_CREATE = - "create table " + FILE_TABLE + " ( " + KEY_FILE_ID + " integer primary key autoincrement, " - + KEY_FILE_FILENAME + " text not null, " + KEY_FILE_KEYFILE + " text, " - + KEY_FILE_UPDATED + " integer not null);"; - - private final Context mCtx; - private DatabaseHelper mDbHelper; - private SQLiteDatabase mDb; - - private static class DatabaseHelper extends SQLiteOpenHelper { - private final Context mCtx; - - DatabaseHelper(Context ctx) { - super(ctx, DATABASE_NAME, null, DATABASE_VERSION); - mCtx = ctx; - } + public static final String LAST_FILENAME = "lastFile"; + public static final String LAST_KEYFILE = "lastKey"; - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL(DATABASE_CREATE); - - // Migrate preference to database if it is set. - SharedPreferences settings = mCtx.getSharedPreferences("PasswordActivity", Context.MODE_PRIVATE); - String lastFile = settings.getString(LAST_FILENAME, ""); - String lastKey = settings.getString(LAST_KEYFILE,""); - - if ( lastFile.length() > 0 ) { - ContentValues vals = new ContentValues(); - vals.put(KEY_FILE_FILENAME, lastFile); - vals.put(KEY_FILE_UPDATED, System.currentTimeMillis()); - - if ( lastKey.length() > 0 ) { - vals.put(KEY_FILE_KEYFILE, lastKey); - } - - db.insert(FILE_TABLE, null, vals); - - // Clear old preferences - deletePrefs(settings); - - } - } + public static final String DATABASE_NAME = "keepassdroid"; // TODO Change db name + private static final String FILE_TABLE = "files"; + private static final int DATABASE_VERSION = 1; + + public static final int MAX_FILES = 5; + + public static final String KEY_FILE_ID = "_id"; + public static final String KEY_FILE_FILENAME = "fileName"; + public static final String KEY_FILE_KEYFILE = "keyFile"; + public static final String KEY_FILE_UPDATED = "updated"; + + private static final String DATABASE_CREATE = + "create table " + FILE_TABLE + " ( " + KEY_FILE_ID + " integer primary key autoincrement, " + + KEY_FILE_FILENAME + " text not null, " + KEY_FILE_KEYFILE + " text, " + + KEY_FILE_UPDATED + " integer not null);"; + + private final Context mCtx; + private DatabaseHelper mDbHelper; + private SQLiteDatabase mDb; + + private static class DatabaseHelper extends SQLiteOpenHelper { + private final Context mCtx; + + DatabaseHelper(Context ctx) { + super(ctx, DATABASE_NAME, null, DATABASE_VERSION); + mCtx = ctx; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(DATABASE_CREATE); + + // Migrate preference to database if it is set. + SharedPreferences settings = mCtx.getSharedPreferences("PasswordActivity", Context.MODE_PRIVATE); + String lastFile = settings.getString(LAST_FILENAME, ""); + String lastKey = settings.getString(LAST_KEYFILE,""); + + if ( lastFile.length() > 0 ) { + ContentValues vals = new ContentValues(); + vals.put(KEY_FILE_FILENAME, lastFile); + vals.put(KEY_FILE_UPDATED, System.currentTimeMillis()); + + if ( lastKey.length() > 0 ) { + vals.put(KEY_FILE_KEYFILE, lastKey); + } + + db.insert(FILE_TABLE, null, vals); + + // Clear old preferences + deletePrefs(settings); + + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Only one database version so far + } + + private void deletePrefs(SharedPreferences prefs) { + // We won't worry too much if this fails + try { + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(LAST_FILENAME); + editor.remove(LAST_KEYFILE); + editor.apply(); + } catch (Exception e) { + Log.e(TAG, "Unable to delete database preference", e); + } + } + } + + public FileDbHelper(Context ctx) { + mCtx = ctx; + } + + public FileDbHelper open() throws SQLException { + mDbHelper = new DatabaseHelper(mCtx); + mDb = mDbHelper.getWritableDatabase(); + return this; + } + + public boolean isOpen() { + return mDb.isOpen(); + } + + public void close() { + mDb.close(); + } + + public long createFile(String fileName, String keyFile) { + + // Check to see if this filename is already used + Cursor cursor; + try { + cursor = mDb.query(true, FILE_TABLE, new String[] {KEY_FILE_ID}, + KEY_FILE_FILENAME + "=?", new String[] {fileName}, null, null, null, null); + } catch (Exception e ) { + assert(true); + return -1; + } + + long result; + // If there is an existing entry update it with the new key file + if ( cursor.getCount() > 0 ) { + cursor.moveToFirst(); + long id = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_FILE_ID)); + + ContentValues vals = new ContentValues(); + vals.put(KEY_FILE_KEYFILE, keyFile); + vals.put(KEY_FILE_UPDATED, System.currentTimeMillis()); + + result = mDb.update(FILE_TABLE, vals, KEY_FILE_ID + " = " + id, null); + + // Otherwise add the new entry + } else { + ContentValues vals = new ContentValues(); + vals.put(KEY_FILE_FILENAME, fileName); + vals.put(KEY_FILE_KEYFILE, keyFile); + vals.put(KEY_FILE_UPDATED, System.currentTimeMillis()); + + result = mDb.insert(FILE_TABLE, null, vals); + + } + // Delete all but the last five records + try { + deleteAllBut(MAX_FILES); + } catch (Exception e) { + e.printStackTrace(); + assert(true); + } + + cursor.close(); + + return result; + + } + + private void deleteAllBut(int limit) { + Cursor cursor = mDb.query(FILE_TABLE, new String[] {KEY_FILE_UPDATED}, null, null, null, null, KEY_FILE_UPDATED); + + if ( cursor.getCount() > limit ) { + cursor.moveToFirst(); + long time = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_FILE_UPDATED)); + + mDb.execSQL("DELETE FROM " + FILE_TABLE + " WHERE " + KEY_FILE_UPDATED + "<" + time + ";"); + } + + cursor.close(); + + } + + public void deleteAllKeys() { + ContentValues vals = new ContentValues(); + vals.put(KEY_FILE_KEYFILE, ""); + + mDb.update(FILE_TABLE, vals, null, null); + } + + public void deleteFile(String filename) { + mDb.delete(FILE_TABLE, KEY_FILE_FILENAME + " = ?", new String[] {filename}); + } + + + public Cursor fetchAllFiles() { + Cursor ret; + ret = mDb.query(FILE_TABLE, new String[] {KEY_FILE_ID, KEY_FILE_FILENAME, KEY_FILE_KEYFILE }, null, null, null, null, KEY_FILE_UPDATED + " DESC", Integer.toString(MAX_FILES)); + return ret; + } + + public Cursor fetchFile(long fileId) throws SQLException { + Cursor cursor = mDb.query(true, FILE_TABLE, new String[] {KEY_FILE_FILENAME, KEY_FILE_KEYFILE}, + KEY_FILE_ID + "=" + fileId, null, null, null, null, null); + + if ( cursor != null ) { + cursor.moveToFirst(); + } + + return cursor; + + } + + public String getFileByName(String name) { + Cursor cursor = mDb.query(true, FILE_TABLE, new String[] {KEY_FILE_KEYFILE}, + KEY_FILE_FILENAME + "= ?", new String[] {name}, null, null, null, null); + + if ( cursor == null ) { + return ""; + } + + String filename; + + if ( cursor.moveToFirst() ) { + filename = cursor.getString(0); + } else { + // Cursor is empty + filename = ""; + } + cursor.close(); + return filename; + } + + public boolean hasRecentFiles() { + Cursor cursor = fetchAllFiles(); + + boolean hasRecent = cursor.getCount() > 0; + cursor.close(); + + return hasRecent; + } - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // Only one database version so far - } - - private void deletePrefs(SharedPreferences prefs) { - // We won't worry too much if this fails - try { - SharedPreferences.Editor editor = prefs.edit(); - editor.remove(LAST_FILENAME); - editor.remove(LAST_KEYFILE); - editor.apply(); - } catch (Exception e) { - Log.e(TAG, "Unable to delete database preference", e); - } - } - } - - public FileDbHelper(Context ctx) { - mCtx = ctx; - } - - public FileDbHelper open() throws SQLException { - mDbHelper = new DatabaseHelper(mCtx); - mDb = mDbHelper.getWritableDatabase(); - return this; - } - - public boolean isOpen() { - return mDb.isOpen(); - } - - public void close() { - mDb.close(); - } - - public long createFile(String fileName, String keyFile) { - - // Check to see if this filename is already used - Cursor cursor; - try { - cursor = mDb.query(true, FILE_TABLE, new String[] {KEY_FILE_ID}, - KEY_FILE_FILENAME + "=?", new String[] {fileName}, null, null, null, null); - } catch (Exception e ) { - assert(true); - return -1; - } - - long result; - // If there is an existing entry update it with the new key file - if ( cursor.getCount() > 0 ) { - cursor.moveToFirst(); - long id = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_FILE_ID)); - - ContentValues vals = new ContentValues(); - vals.put(KEY_FILE_KEYFILE, keyFile); - vals.put(KEY_FILE_UPDATED, System.currentTimeMillis()); - - result = mDb.update(FILE_TABLE, vals, KEY_FILE_ID + " = " + id, null); - - // Otherwise add the new entry - } else { - ContentValues vals = new ContentValues(); - vals.put(KEY_FILE_FILENAME, fileName); - vals.put(KEY_FILE_KEYFILE, keyFile); - vals.put(KEY_FILE_UPDATED, System.currentTimeMillis()); - - result = mDb.insert(FILE_TABLE, null, vals); - - } - // Delete all but the last five records - try { - deleteAllBut(MAX_FILES); - } catch (Exception e) { - e.printStackTrace(); - assert(true); - } - - cursor.close(); - - return result; - - } - - private void deleteAllBut(int limit) { - Cursor cursor = mDb.query(FILE_TABLE, new String[] {KEY_FILE_UPDATED}, null, null, null, null, KEY_FILE_UPDATED); - - if ( cursor.getCount() > limit ) { - cursor.moveToFirst(); - long time = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_FILE_UPDATED)); - - mDb.execSQL("DELETE FROM " + FILE_TABLE + " WHERE " + KEY_FILE_UPDATED + "<" + time + ";"); - } - - cursor.close(); - - } - - public void deleteAllKeys() { - ContentValues vals = new ContentValues(); - vals.put(KEY_FILE_KEYFILE, ""); - - mDb.update(FILE_TABLE, vals, null, null); - } - - public void deleteFile(String filename) { - mDb.delete(FILE_TABLE, KEY_FILE_FILENAME + " = ?", new String[] {filename}); - } - - - public Cursor fetchAllFiles() { - Cursor ret; - ret = mDb.query(FILE_TABLE, new String[] {KEY_FILE_ID, KEY_FILE_FILENAME, KEY_FILE_KEYFILE }, null, null, null, null, KEY_FILE_UPDATED + " DESC", Integer.toString(MAX_FILES)); - return ret; - } - - public Cursor fetchFile(long fileId) throws SQLException { - Cursor cursor = mDb.query(true, FILE_TABLE, new String[] {KEY_FILE_FILENAME, KEY_FILE_KEYFILE}, - KEY_FILE_ID + "=" + fileId, null, null, null, null, null); - - if ( cursor != null ) { - cursor.moveToFirst(); - } - - return cursor; - - } - - public String getFileByName(String name) { - Cursor cursor = mDb.query(true, FILE_TABLE, new String[] {KEY_FILE_KEYFILE}, - KEY_FILE_FILENAME + "= ?", new String[] {name}, null, null, null, null); - - if ( cursor == null ) { - return ""; - } - - String filename; - - if ( cursor.moveToFirst() ) { - filename = cursor.getString(0); - } else { - // Cursor is empty - filename = ""; - } - cursor.close(); - return filename; - } - - public boolean hasRecentFiles() { - Cursor cursor = fetchAllFiles(); - - boolean hasRecent = cursor.getCount() > 0; - cursor.close(); - - return hasRecent; - } - /** * Deletes a database including its journal file and other auxiliary files * that may have been created by the database engine. @@ -257,7 +257,7 @@ public class FileDbHelper { * @return True if the database was successfully deleted. */ public static boolean deleteDatabase(Context ctx) { - File file = ctx.getDatabasePath(DATABASE_NAME); + File file = ctx.getDatabasePath(DATABASE_NAME); if (file == null) { throw new IllegalArgumentException("file must not be null"); } diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java index 5430c444c..1c7d4c2ea 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java @@ -1,6 +1,6 @@ /* * 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 @@ -86,7 +86,7 @@ import permissions.dispatcher.RuntimePermissions; public class FileSelectActivity extends StylishActivity implements CreateFileDialogFragment.DefinePathDialogListener, AssignMasterKeyDialogFragment.AssignPasswordDialogListener, - FileSelectAdapter.FileItemOpenListener, + FileSelectAdapter.FileItemOpenListener, FileSelectAdapter.FileSelectClearListener, FileSelectAdapter.FileInformationShowListener { @@ -95,64 +95,64 @@ public class FileSelectActivity extends StylishActivity implements private static final String EXTRA_STAY = "EXTRA_STAY"; private FileSelectAdapter mAdapter; - private View fileListContainer; - private View createButtonView; - private View browseButtonView; - private View openButtonView; + private View fileListContainer; + private View createButtonView; + private View browseButtonView; + private View openButtonView; - private RecentFileHistory fileHistory; + private RecentFileHistory fileHistory; private View fileSelectExpandableButton; private ExpandableLayout fileSelectExpandable; - private EditText openFileNameView; + private EditText openFileNameView; - private Uri databaseUri; + private Uri databaseUri; - private KeyFileHelper keyFileHelper; + private KeyFileHelper keyFileHelper; private String defaultPath; - /* - * ------------------------- - * No Standard Launch, pass by PasswordActivity - * ------------------------- - */ + /* + * ------------------------- + * No Standard Launch, pass by PasswordActivity + * ------------------------- + */ - /* - * ------------------------- - * Keyboard Launch - * ------------------------- - */ + /* + * ------------------------- + * Keyboard Launch + * ------------------------- + */ public static void launchForKeyboardSelection(Activity activity) { - KeyboardHelper.INSTANCE.startActivityForKeyboardSelection(activity, new Intent(activity, FileSelectActivity.class)); + KeyboardHelper.INSTANCE.startActivityForKeyboardSelection(activity, new Intent(activity, FileSelectActivity.class)); } - /* - * ------------------------- - * Autofill Launch - * ------------------------- - */ + /* + * ------------------------- + * Autofill Launch + * ------------------------- + */ - @RequiresApi(api = Build.VERSION_CODES.O) - public static void launchForAutofillResult(Activity activity, @NonNull AssistStructure assistStructure) { - AutofillHelper.INSTANCE.startActivityForAutofillResult(activity, - new Intent(activity, FileSelectActivity.class), - assistStructure); - } + @RequiresApi(api = Build.VERSION_CODES.O) + public static void launchForAutofillResult(Activity activity, @NonNull AssistStructure assistStructure) { + AutofillHelper.INSTANCE.startActivityForAutofillResult(activity, + new Intent(activity, FileSelectActivity.class), + assistStructure); + } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - fileHistory = App.getFileHistory(); + fileHistory = App.getFileHistory(); setContentView(R.layout.file_selection); fileListContainer = findViewById(R.id.container_file_list); - Toolbar toolbar = findViewById(R.id.toolbar); - toolbar.setTitle(""); - setSupportActionBar(toolbar); + Toolbar toolbar = findViewById(R.id.toolbar); + toolbar.setTitle(""); + setSupportActionBar(toolbar); openFileNameView = findViewById(R.id.file_filename); @@ -175,37 +175,37 @@ public class FileSelectActivity extends StylishActivity implements // History list RecyclerView mListFiles = findViewById(R.id.file_list); - mListFiles.setLayoutManager(new LinearLayoutManager(this)); + mListFiles.setLayoutManager(new LinearLayoutManager(this)); - // Open button - openButtonView = findViewById(R.id.open_database); + // Open button + openButtonView = findViewById(R.id.open_database); openButtonView.setOnClickListener(v -> { - String fileName = openFileNameView.getText().toString(); + String fileName = openFileNameView.getText().toString(); if (fileName.isEmpty()) fileName = defaultPath; launchPasswordActivityWithPath(fileName); }); - // Create button - createButtonView = findViewById(R.id.create_database); + // Create button + createButtonView = findViewById(R.id.create_database); createButtonView .setOnClickListener(v -> FileSelectActivityPermissionsDispatcher .openCreateFileDialogFragmentWithPermissionCheck(FileSelectActivity.this) - ); + ); keyFileHelper = new KeyFileHelper(this); - browseButtonView = findViewById(R.id.browse_button); + browseButtonView = findViewById(R.id.browse_button); browseButtonView.setOnClickListener(keyFileHelper.getOpenFileOnClickViewListener( () -> Uri.parse("file://" + openFileNameView.getText().toString()))); - // Construct adapter with listeners - mAdapter = new FileSelectAdapter(FileSelectActivity.this, fileHistory.getDbList()); - mAdapter.setOnItemClickListener(this); - mAdapter.setFileSelectClearListener(this); - mAdapter.setFileInformationShowListener(this); - mListFiles.setAdapter(mAdapter); + // Construct adapter with listeners + mAdapter = new FileSelectAdapter(FileSelectActivity.this, fileHistory.getDbList()); + mAdapter.setOnItemClickListener(this); + mAdapter.setFileSelectClearListener(this); + mAdapter.setFileInformationShowListener(this); + mListFiles.setAdapter(mAdapter); - // Load default database if not an orientation change + // Load default database if not an orientation change if (! (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_STAY) && savedInstanceState.getBoolean(EXTRA_STAY, false)) ) { @@ -234,55 +234,55 @@ public class FileSelectActivity extends StylishActivity implements // For the first time show education checkAndPerformedEducation(); - } + } - private void fileNoFoundAction(FileNotFoundException e) { - String error = getString(R.string.file_not_found_content); - Toast.makeText(FileSelectActivity.this, - error, Toast.LENGTH_LONG).show(); - Log.e(TAG, error, e); - } + private void fileNoFoundAction(FileNotFoundException e) { + String error = getString(R.string.file_not_found_content); + Toast.makeText(FileSelectActivity.this, + error, Toast.LENGTH_LONG).show(); + Log.e(TAG, error, e); + } - private void launchPasswordActivity(String fileName, String keyFile) { - EntrySelectionHelper.INSTANCE.doEntrySelectionAction(getIntent(), - () -> { - try { - PasswordActivity.launch(FileSelectActivity.this, - fileName, keyFile); - } catch (FileNotFoundException e) { - fileNoFoundAction(e); - } - return null; - }, - () -> { - try { - PasswordActivity.launchForKeyboardResult(FileSelectActivity.this, - fileName, keyFile); - finish(); - } catch (FileNotFoundException e) { - fileNoFoundAction(e); - } - return null; - }, - assistStructure -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - PasswordActivity.launchForAutofillResult(FileSelectActivity.this, - fileName, keyFile, - assistStructure); - } catch (FileNotFoundException e) { - fileNoFoundAction(e); - } - } - return null; - }); - } + private void launchPasswordActivity(String fileName, String keyFile) { + EntrySelectionHelper.INSTANCE.doEntrySelectionAction(getIntent(), + () -> { + try { + PasswordActivity.launch(FileSelectActivity.this, + fileName, keyFile); + } catch (FileNotFoundException e) { + fileNoFoundAction(e); + } + return null; + }, + () -> { + try { + PasswordActivity.launchForKeyboardResult(FileSelectActivity.this, + fileName, keyFile); + finish(); + } catch (FileNotFoundException e) { + fileNoFoundAction(e); + } + return null; + }, + assistStructure -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + PasswordActivity.launchForAutofillResult(FileSelectActivity.this, + fileName, keyFile, + assistStructure); + } catch (FileNotFoundException e) { + fileNoFoundAction(e); + } + } + return null; + }); + } - private void launchPasswordActivityWithPath(String path) { - launchPasswordActivity(path, ""); - // Delete flickering for kitkat <= - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) - overridePendingTransition(0, 0); + private void launchPasswordActivityWithPath(String path) { + launchPasswordActivity(path, ""); + // Delete flickering for kitkat <= + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + overridePendingTransition(0, 0); } private void updateExternalStorageWarning() { @@ -440,10 +440,10 @@ public class FileSelectActivity extends StylishActivity implements createFileDialogFragment.show(getSupportFragmentManager(), "createFileDialogFragment"); } - private void updateFileListVisibility() { - if(mAdapter.getItemCount() == 0) + private void updateFileListVisibility() { + if(mAdapter.getItemCount() == 0) fileListContainer.setVisibility(View.INVISIBLE); - else + else fileListContainer.setVisibility(View.VISIBLE); } @@ -451,64 +451,64 @@ public class FileSelectActivity extends StylishActivity implements * Create file for database * @return If not created, return false */ - private boolean createDatabaseFile(Uri path) { + private boolean createDatabaseFile(Uri path) { - String pathString = URLDecoder.decode(path.getPath()); - // Make sure file name exists - if (pathString.length() == 0) { - Log.e(TAG, getString(R.string.error_filename_required)); - Toast.makeText(FileSelectActivity.this, - R.string.error_filename_required, - Toast.LENGTH_LONG).show(); - return false; - } + String pathString = URLDecoder.decode(path.getPath()); + // Make sure file name exists + if (pathString.length() == 0) { + Log.e(TAG, getString(R.string.error_filename_required)); + Toast.makeText(FileSelectActivity.this, + R.string.error_filename_required, + Toast.LENGTH_LONG).show(); + return false; + } - // Try to create the file - File file = new File(pathString); - try { - if (file.exists()) { + // Try to create the file + File file = new File(pathString); + try { + if (file.exists()) { Log.e(TAG, getString(R.string.error_database_exists) + " " + file); - Toast.makeText(FileSelectActivity.this, - R.string.error_database_exists, - Toast.LENGTH_LONG).show(); - return false; - } - File parent = file.getParentFile(); + Toast.makeText(FileSelectActivity.this, + R.string.error_database_exists, + Toast.LENGTH_LONG).show(); + return false; + } + File parent = file.getParentFile(); - if ( parent == null || (parent.exists() && ! parent.isDirectory()) ) { + if ( parent == null || (parent.exists() && ! parent.isDirectory()) ) { Log.e(TAG, getString(R.string.error_invalid_path) + " " + file); - Toast.makeText(FileSelectActivity.this, - R.string.error_invalid_path, - Toast.LENGTH_LONG).show(); - return false; - } + Toast.makeText(FileSelectActivity.this, + R.string.error_invalid_path, + Toast.LENGTH_LONG).show(); + return false; + } - if ( ! parent.exists() ) { - // Create parent directory - if ( ! parent.mkdirs() ) { + if ( ! parent.exists() ) { + // Create parent directory + if ( ! parent.mkdirs() ) { Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + parent); - Toast.makeText(FileSelectActivity.this, - R.string.error_could_not_create_parent, - Toast.LENGTH_LONG).show(); - return false; - } - } + Toast.makeText(FileSelectActivity.this, + R.string.error_could_not_create_parent, + Toast.LENGTH_LONG).show(); + return false; + } + } - return file.createNewFile(); - } catch (IOException e) { + return file.createNewFile(); + } catch (IOException e) { Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + e.getLocalizedMessage()); e.printStackTrace(); - Toast.makeText( - FileSelectActivity.this, - getText(R.string.error_file_not_create) + " " - + e.getLocalizedMessage(), - Toast.LENGTH_LONG).show(); - return false; - } - } + Toast.makeText( + FileSelectActivity.this, + getText(R.string.error_file_not_create) + " " + + e.getLocalizedMessage(), + Toast.LENGTH_LONG).show(); + return false; + } + } - @Override - public boolean onDefinePathDialogPositiveClick(Uri pathFile) { + @Override + public boolean onDefinePathDialogPositiveClick(Uri pathFile) { databaseUri = pathFile; if(createDatabaseFile(pathFile)) { AssignMasterKeyDialogFragment assignMasterKeyDialogFragment = new AssignMasterKeyDialogFragment(); @@ -516,97 +516,97 @@ public class FileSelectActivity extends StylishActivity implements return true; } else return false; - } + } - @Override - public boolean onDefinePathDialogNegativeClick(Uri pathFile) { + @Override + public boolean onDefinePathDialogNegativeClick(Uri pathFile) { return true; - } + } - @Override - public void onAssignKeyDialogPositiveClick( - boolean masterPasswordChecked, String masterPassword, - boolean keyFileChecked, Uri keyFile) { + @Override + public void onAssignKeyDialogPositiveClick( + boolean masterPasswordChecked, String masterPassword, + boolean keyFileChecked, Uri keyFile) { - try { - String databaseFilename = databaseUri.getPath(); + try { + String databaseFilename = databaseUri.getPath(); - if (databaseFilename != null) { - // Create the new database and start prof - new Thread(new ProgressDialogRunnable(this, - R.string.progress_create, - progressTaskUpdater -> - new CreateDatabaseRunnable(databaseFilename, database -> { - // TODO store database created - return new AssignPasswordInDatabaseRunnable(FileSelectActivity.this, - database, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile, - new LaunchGroupActivityFinish(UriUtil.parseDefaultFile(databaseFilename)), - true // TODO get readonly - ); - }) - )).start(); - } - } catch (Exception e) { - String error = "Unable to create database with this password and key file"; - Toast.makeText(this, error, Toast.LENGTH_LONG).show(); - Log.e(TAG, error + " " + e.getMessage()); - // TODO remove - e.printStackTrace(); - } - } + if (databaseFilename != null) { + // Create the new database and start prof + new Thread(new ProgressDialogRunnable(this, + R.string.progress_create, + progressTaskUpdater -> + new CreateDatabaseRunnable(databaseFilename, database -> { + // TODO store database created + return new AssignPasswordInDatabaseRunnable(FileSelectActivity.this, + database, + masterPasswordChecked, + masterPassword, + keyFileChecked, + keyFile, + new LaunchGroupActivityFinish(UriUtil.parseDefaultFile(databaseFilename)), + true // TODO get readonly + ); + }) + )).start(); + } + } catch (Exception e) { + String error = "Unable to create database with this password and key file"; + Toast.makeText(this, error, Toast.LENGTH_LONG).show(); + Log.e(TAG, error + " " + e.getMessage()); + // TODO remove + e.printStackTrace(); + } + } - private class LaunchGroupActivityFinish extends ActionRunnable { + private class LaunchGroupActivityFinish extends ActionRunnable { - private Uri fileURI; + private Uri fileURI; - LaunchGroupActivityFinish(Uri fileUri) { - super(); - this.fileURI = fileUri; - } + LaunchGroupActivityFinish(Uri fileUri) { + super(); + this.fileURI = fileUri; + } - @Override - public void run() { - finishRun(true, null); - } + @Override + public void run() { + finishRun(true, null); + } - @Override - public void onFinishRun(boolean isSuccess, @Nullable String message) { - runOnUiThread(() -> { - if (isSuccess) { - // Add database to recent files - fileHistory.createFile(fileURI); - mAdapter.notifyDataSetChanged(); - updateFileListVisibility(); - GroupActivity.launch(FileSelectActivity.this); - } else { - Log.e(TAG, "Unable to open the database"); - } - }); - } - } + @Override + public void onFinishRun(boolean isSuccess, @Nullable String message) { + runOnUiThread(() -> { + if (isSuccess) { + // Add database to recent files + fileHistory.createFile(fileURI); + mAdapter.notifyDataSetChanged(); + updateFileListVisibility(); + GroupActivity.launch(FileSelectActivity.this); + } else { + Log.e(TAG, "Unable to open the database"); + } + }); + } + } - @Override - public void onAssignKeyDialogNegativeClick( - boolean masterPasswordChecked, String masterPassword, - boolean keyFileChecked, Uri keyFile) { + @Override + public void onAssignKeyDialogNegativeClick( + boolean masterPasswordChecked, String masterPassword, + boolean keyFileChecked, Uri keyFile) { - } + } - @Override - public void onFileItemOpenListener(int itemPosition) { - new OpenFileHistoryAsyncTask((fileName, keyFile) -> { - launchPasswordActivity(fileName, keyFile); + @Override + public void onFileItemOpenListener(int itemPosition) { + new OpenFileHistoryAsyncTask((fileName, keyFile) -> { + launchPasswordActivity(fileName, keyFile); updateFileListVisibility(); }, fileHistory).execute(itemPosition); - } + } @Override public void onClickFileInformation(FileSelectBean fileSelectBean) { - if (fileSelectBean != null) { + if (fileSelectBean != null) { FileInformationDialogFragment fileInformationDialogFragment = FileInformationDialogFragment.newInstance(fileSelectBean); fileInformationDialogFragment.show(getSupportFragmentManager(), "fileInformation"); @@ -623,15 +623,15 @@ public class FileSelectActivity extends StylishActivity implements return true; } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.INSTANCE.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data); - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + AutofillHelper.INSTANCE.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data); + } - keyFileHelper.onActivityResultCallback(requestCode, resultCode, data, + keyFileHelper.onActivityResultCallback(requestCode, resultCode, data, uri -> { if (uri != null) { if (PreferencesUtil.autoOpenSelectedFile(FileSelectActivity.this)) { @@ -642,7 +642,7 @@ public class FileSelectActivity extends StylishActivity implements } } }); - } + } @OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE) void showRationaleForExternalStorage(final PermissionRequest request) { @@ -663,16 +663,16 @@ public class FileSelectActivity extends StylishActivity implements Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show(); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - MenuUtil.INSTANCE.defaultMenuInflater(getMenuInflater(), menu); - return true; - } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuUtil.INSTANCE.defaultMenuInflater(getMenuInflater(), menu); + return true; + } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - return MenuUtil.INSTANCE.onDefaultMenuOptionsItemSelected(this, item) - && super.onOptionsItemSelected(item); - } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return MenuUtil.INSTANCE.onDefaultMenuOptionsItemSelected(this, item) + && super.onOptionsItemSelected(item); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.java b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.java index cffa1bd1f..f878e6494 100644 --- a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.java +++ b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.java @@ -1,6 +1,6 @@ /* * 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 @@ -48,21 +48,21 @@ public class IconDrawableFactory { private static final String TAG = IconDrawableFactory.class.getName(); - private static Drawable blank = null; - private static int blankWidth = -1; - private static int blankHeight = -1; - - /** customIconMap - * Cache for icon drawable. - * Keys: UUID, Values: Drawables - */ - private ReferenceMap customIconMap = new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK); - - /** standardIconMap - * Cache for icon drawable. - * Keys: Integer, Values: Drawables - */ - private ReferenceMap standardIconMap = new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK); + private static Drawable blank = null; + private static int blankWidth = -1; + private static int blankHeight = -1; + + /** customIconMap + * Cache for icon drawable. + * Keys: UUID, Values: Drawables + */ + private ReferenceMap customIconMap = new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK); + + /** standardIconMap + * Cache for icon drawable. + * Keys: Integer, Values: Drawables + */ + private ReferenceMap standardIconMap = new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK); /** * Assign a default database icon to an ImageView and tint it if needed @@ -95,19 +95,19 @@ public class IconDrawableFactory { * @param icon The icon from the database * @param tintColor Use this color to tint tintable icon */ - public void assignDatabaseIconTo(Context context, ImageView iconView, PwIcon icon, int tintColor) { - if (IconPackChooser.getSelectedIconPack(context).tintable()) { + public void assignDatabaseIconTo(Context context, ImageView iconView, PwIcon icon, int tintColor) { + if (IconPackChooser.getSelectedIconPack(context).tintable()) { assignDrawableToImageView(getIconDrawable(context, icon, true, tintColor), iconView, true, tintColor); - } else { + } else { assignDrawableToImageView(getIconDrawable(context, icon, true, tintColor), iconView, false, Color.WHITE); - } - } + } + } /** * Assign an image by its resourceId to an ImageView and tint it @@ -146,9 +146,9 @@ public class IconDrawableFactory { * @param icon The icon from database * @return The build drawable */ - public Drawable getIconDrawable(Context context, PwIcon icon) { - return getIconDrawable(context, icon, false, Color.WHITE).drawable; - } + public Drawable getIconDrawable(Context context, PwIcon icon) { + return getIconDrawable(context, icon, false, Color.WHITE).drawable; + } /** * Get the drawable icon from cache or build it and add it to the cache if not exists yet then tint it if needed @@ -171,27 +171,27 @@ public class IconDrawableFactory { * Build a blank drawable * @param res Resource to build the drawable */ - private static void initBlank(Resources res) { - if (blank==null) { - blankWidth = (int) res.getDimension(R.dimen.icon_size); - blankHeight = (int) res.getDimension(R.dimen.icon_size); - blank = new ColorDrawable(Color.TRANSPARENT); - blank.setBounds(0, 0, blankWidth, blankHeight); - } - } + private static void initBlank(Resources res) { + if (blank==null) { + blankWidth = (int) res.getDimension(R.dimen.icon_size); + blankHeight = (int) res.getDimension(R.dimen.icon_size); + blank = new ColorDrawable(Color.TRANSPARENT); + blank.setBounds(0, 0, blankWidth, blankHeight); + } + } /** * Key class to retrieve a Drawable in the cache if it's tinted or not */ - private class CacheKey { - int resId; - boolean isTint; - int color; + private class CacheKey { + int resId; + boolean isTint; + int color; - CacheKey(int resId, boolean isTint, int color) { - this.resId = resId; - this.isTint = isTint; - this.color = color; + CacheKey(int resId, boolean isTint, int color) { + this.resId = resId; + this.isTint = isTint; + this.color = color; } @Override @@ -202,7 +202,7 @@ public class IconDrawableFactory { if (isTint) return resId == cacheKey.resId && cacheKey.isTint && - color == cacheKey.color; + color == cacheKey.color; else return resId == cacheKey.resId && !cacheKey.isTint; @@ -218,11 +218,11 @@ public class IconDrawableFactory { * @param tintColor Use this color if tint is true * @return The drawable */ - private Drawable getIconDrawable(Context context, PwIconStandard icon, boolean isTint, int tintColor) { - int resId = IconPackChooser.getSelectedIconPack(context).iconToResId(icon.getIconId()); + private Drawable getIconDrawable(Context context, PwIconStandard icon, boolean isTint, int tintColor) { + int resId = IconPackChooser.getSelectedIconPack(context).iconToResId(icon.getIconId()); - return getIconDrawable(context, resId, isTint, tintColor); - } + return getIconDrawable(context, resId, isTint, tintColor); + } /** * Get the drawable icon from cache or build it and add it to the cache if not exists yet @@ -257,23 +257,23 @@ public class IconDrawableFactory { return draw; } - /** - * Utility class to prevent a custom icon to be tint - */ - private class SuperDrawable { - Drawable drawable; - boolean custom; + /** + * Utility class to prevent a custom icon to be tint + */ + private class SuperDrawable { + Drawable drawable; + boolean custom; - SuperDrawable(Drawable drawable) { - this.drawable = drawable; - this.custom = false; + SuperDrawable(Drawable drawable) { + this.drawable = drawable; + this.custom = false; } SuperDrawable(Drawable drawable, boolean custom) { this.drawable = drawable; this.custom = custom; } - } + } /** * Build a custom icon from database @@ -281,55 +281,55 @@ public class IconDrawableFactory { * @param icon Icon from database * @return The drawable */ - private Drawable getIconDrawable(Context context, PwIconCustom icon) { - initBlank(context.getResources()); - if (icon == null) { - return blank; - } - - Drawable draw = (Drawable) customIconMap.get(icon.getUuid()); - - if (draw == null) { + private Drawable getIconDrawable(Context context, PwIconCustom icon) { + initBlank(context.getResources()); + if (icon == null) { + return blank; + } - Bitmap bitmap = BitmapFactory.decodeByteArray(icon.getImageData(), 0, icon.getImageData().length); - - // Could not understand custom icon - if (bitmap == null) { - return blank; - } - - bitmap = resize(bitmap); - - draw = new BitmapDrawable(context.getResources(), bitmap); - customIconMap.put(icon.getUuid(), draw); - } + Drawable draw = (Drawable) customIconMap.get(icon.getUuid()); - return draw; - } - - /** + if (draw == null) { + + Bitmap bitmap = BitmapFactory.decodeByteArray(icon.getImageData(), 0, icon.getImageData().length); + + // Could not understand custom icon + if (bitmap == null) { + return blank; + } + + bitmap = resize(bitmap); + + draw = new BitmapDrawable(context.getResources(), bitmap); + customIconMap.put(icon.getUuid(), draw); + } + + return draw; + } + + /** * Resize the custom icon to match the built in icons * - * @param bitmap Bitmap to resize - * @return Bitmap resized - */ - private Bitmap resize(Bitmap bitmap) { - int width = bitmap.getWidth(); - int height = bitmap.getHeight(); - - if (width == blankWidth && height == blankHeight) { - return bitmap; - } - - return Bitmap.createScaledBitmap(bitmap, blankWidth, blankHeight, true); - } + * @param bitmap Bitmap to resize + * @return Bitmap resized + */ + private Bitmap resize(Bitmap bitmap) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + + if (width == blankWidth && height == blankHeight) { + return bitmap; + } + + return Bitmap.createScaledBitmap(bitmap, blankWidth, blankHeight, true); + } /** * Clear the cache of icons */ - public void clearCache() { - standardIconMap.clear(); - customIconMap.clear(); - } - + public void clearCache() { + standardIconMap.clear(); + customIconMap.clear(); + } + } diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/view/MagikeyboardView.java b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/view/MagikeyboardView.java index b962f4b62..5b4ea4151 100644 --- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/view/MagikeyboardView.java +++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/view/MagikeyboardView.java @@ -12,28 +12,28 @@ import static com.kunzisoft.keepass.magikeyboard.MagikIME.KEY_CHANGE_KEYBOARD; public class MagikeyboardView extends KeyboardView { - public MagikeyboardView(Context context, AttributeSet attrs) { - super(context, attrs); - } + public MagikeyboardView(Context context, AttributeSet attrs) { + super(context, attrs); + } - public MagikeyboardView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } + public MagikeyboardView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public MagikeyboardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public MagikeyboardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } - @Override - protected boolean onLongPress(Keyboard.Key key) { - // TODO Action on long press - if (key.codes[0] == KEY_BACK_KEYBOARD) { - getOnKeyboardActionListener().onKey(KEY_CHANGE_KEYBOARD, null); - return true; - } else { - //Log.d("LatinKeyboardView", "KEY: " + key.codes[0]); - return super.onLongPress(key); - } - } + @Override + protected boolean onLongPress(Keyboard.Key key) { + // TODO Action on long press + if (key.codes[0] == KEY_BACK_KEYBOARD) { + getOnKeyboardActionListener().onKey(KEY_CHANGE_KEYBOARD, null); + return true; + } else { + //Log.d("LatinKeyboardView", "KEY: " + key.codes[0]); + return super.onLongPress(key); + } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/password/PasswordGenerator.java b/app/src/main/java/com/kunzisoft/keepass/password/PasswordGenerator.java index a59482314..7a9f352ff 100644 --- a/app/src/main/java/com/kunzisoft/keepass/password/PasswordGenerator.java +++ b/app/src/main/java/com/kunzisoft/keepass/password/PasswordGenerator.java @@ -1,6 +1,6 @@ /* * 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 @@ -26,14 +26,14 @@ import com.kunzisoft.keepass.R; import java.security.SecureRandom; public class PasswordGenerator { - private static final String UPPERCASE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - private static final String LOWERCASE_CHARS = "abcdefghijklmnopqrstuvwxyz"; - private static final String DIGIT_CHARS = "0123456789"; - private static final String MINUS_CHAR = "-"; - private static final String UNDERLINE_CHAR = "_"; - private static final String SPACE_CHAR = " "; - private static final String SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"; - private static final String BRACKET_CHARS = "[]{}()<>"; + private static final String UPPERCASE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String LOWERCASE_CHARS = "abcdefghijklmnopqrstuvwxyz"; + private static final String DIGIT_CHARS = "0123456789"; + private static final String MINUS_CHAR = "-"; + private static final String UNDERLINE_CHAR = "_"; + private static final String SPACE_CHAR = " "; + private static final String SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"; + private static final String BRACKET_CHARS = "[]{}()<>"; // From KeePassXC code https://github.com/keepassxreboot/keepassxc/pull/538 private String extendedChars() { @@ -48,14 +48,14 @@ public class PasswordGenerator { charSet.append('\u00FF'); return charSet.toString(); } - - private Context cxt; - - public PasswordGenerator(Context cxt) { - this.cxt = cxt; - } - - public String generatePassword(int length, + + private Context cxt; + + public PasswordGenerator(Context cxt) { + this.cxt = cxt; + } + + public String generatePassword(int length, boolean upperCase, boolean lowerCase, boolean digits, @@ -65,13 +65,13 @@ public class PasswordGenerator { boolean specials, boolean brackets, boolean extended) throws IllegalArgumentException{ - // Desired password length is 0 or less - if (length <= 0) { - throw new IllegalArgumentException(cxt.getString(R.string.error_wrong_length)); - } - - // No option has been checked - if ( !upperCase + // Desired password length is 0 or less + if (length <= 0) { + throw new IllegalArgumentException(cxt.getString(R.string.error_wrong_length)); + } + + // No option has been checked + if ( !upperCase && !lowerCase && !digits && !minus @@ -80,11 +80,11 @@ public class PasswordGenerator { && !specials && !brackets && !extended) { - throw new IllegalArgumentException(cxt.getString(R.string.error_pass_gen_type)); - } - - String characterSet = getCharacterSet( - upperCase, + throw new IllegalArgumentException(cxt.getString(R.string.error_pass_gen_type)); + } + + String characterSet = getCharacterSet( + upperCase, lowerCase, digits, minus, @@ -93,68 +93,68 @@ public class PasswordGenerator { specials, brackets, extended); - - int size = characterSet.length(); - - StringBuilder buffer = new StringBuilder(); - SecureRandom random = new SecureRandom(); // use more secure variant of Random! - if (size > 0) { - for (int i = 0; i < length; i++) { - char c = characterSet.charAt((char) random.nextInt(size)); - buffer.append(c); - } - } - return buffer.toString(); - } - - private String getCharacterSet(boolean upperCase, - boolean lowerCase, - boolean digits, - boolean minus, - boolean underline, - boolean space, - boolean specials, - boolean brackets, - boolean extended) { - StringBuilder charSet = new StringBuilder(); - - if (upperCase) { - charSet.append(UPPERCASE_CHARS); - } - - if (lowerCase) { - charSet.append(LOWERCASE_CHARS); - } - - if (digits) { - charSet.append(DIGIT_CHARS); - } - - if (minus) { - charSet.append(MINUS_CHAR); - } - - if (underline) { - charSet.append(UNDERLINE_CHAR); - } - - if (space) { - charSet.append(SPACE_CHAR); - } - - if (specials) { - charSet.append(SPECIAL_CHARS); - } - - if (brackets) { - charSet.append(BRACKET_CHARS); - } + int size = characterSet.length(); - if (extended) { + StringBuilder buffer = new StringBuilder(); + + SecureRandom random = new SecureRandom(); // use more secure variant of Random! + if (size > 0) { + for (int i = 0; i < length; i++) { + char c = characterSet.charAt((char) random.nextInt(size)); + buffer.append(c); + } + } + return buffer.toString(); + } + + private String getCharacterSet(boolean upperCase, + boolean lowerCase, + boolean digits, + boolean minus, + boolean underline, + boolean space, + boolean specials, + boolean brackets, + boolean extended) { + StringBuilder charSet = new StringBuilder(); + + if (upperCase) { + charSet.append(UPPERCASE_CHARS); + } + + if (lowerCase) { + charSet.append(LOWERCASE_CHARS); + } + + if (digits) { + charSet.append(DIGIT_CHARS); + } + + if (minus) { + charSet.append(MINUS_CHAR); + } + + if (underline) { + charSet.append(UNDERLINE_CHAR); + } + + if (space) { + charSet.append(SPACE_CHAR); + } + + if (specials) { + charSet.append(SPECIAL_CHARS); + } + + if (brackets) { + charSet.append(BRACKET_CHARS); + } + + if (extended) { charSet.append(extendedChars()); } - return charSet.toString(); - } + return charSet.toString(); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/CopyInputStream.java b/app/src/main/java/com/kunzisoft/keepass/stream/CopyInputStream.java index 52fe74eea..e9d9c73c9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/CopyInputStream.java +++ b/app/src/main/java/com/kunzisoft/keepass/stream/CopyInputStream.java @@ -1,6 +1,6 @@ /* * 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 @@ -28,76 +28,76 @@ import java.io.OutputStream; * output stream. */ public class CopyInputStream extends InputStream { - private InputStream is; - private OutputStream os; - - public CopyInputStream(InputStream is, OutputStream os) { - this.is = is; - this.os = os; - } + private InputStream is; + private OutputStream os; - @Override - public int available() throws IOException { - return is.available(); - } + public CopyInputStream(InputStream is, OutputStream os) { + this.is = is; + this.os = os; + } - @Override - public void close() throws IOException { - is.close(); - os.close(); - } + @Override + public int available() throws IOException { + return is.available(); + } - @Override - public void mark(int readlimit) { - is.mark(readlimit); - } + @Override + public void close() throws IOException { + is.close(); + os.close(); + } - @Override - public boolean markSupported() { - return is.markSupported(); - } + @Override + public void mark(int readlimit) { + is.mark(readlimit); + } - @Override - public int read() throws IOException { - int data = is.read(); - - if (data != -1) { - os.write(data); - } - - return data; - } + @Override + public boolean markSupported() { + return is.markSupported(); + } - @Override - public int read(byte[] b, int offset, int length) throws IOException { - int len = is.read(b, offset, length); - - if (len != -1) { - os.write(b, offset, len); - } - - return len; - } + @Override + public int read() throws IOException { + int data = is.read(); - @Override - public int read(byte[] b) throws IOException { - int len = is.read(b); - - if (len != -1) { - os.write(b, 0, len); - } - - return len; - } + if (data != -1) { + os.write(data); + } - @Override - public synchronized void reset() throws IOException { - is.reset(); - } + return data; + } - @Override - public long skip(long byteCount) throws IOException { - return is.skip(byteCount); - } + @Override + public int read(byte[] b, int offset, int length) throws IOException { + int len = is.read(b, offset, length); + + if (len != -1) { + os.write(b, offset, len); + } + + return len; + } + + @Override + public int read(byte[] b) throws IOException { + int len = is.read(b); + + if (len != -1) { + os.write(b, 0, len); + } + + return len; + } + + @Override + public synchronized void reset() throws IOException { + is.reset(); + } + + @Override + public long skip(long byteCount) throws IOException { + return is.skip(byteCount); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/CountInputStream.java b/app/src/main/java/com/kunzisoft/keepass/stream/CountInputStream.java index 604a93b19..de507a964 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/CountInputStream.java +++ b/app/src/main/java/com/kunzisoft/keepass/stream/CountInputStream.java @@ -1,6 +1,6 @@ /* * 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 @@ -23,60 +23,60 @@ import java.io.IOException; import java.io.InputStream; public class CountInputStream extends InputStream { - InputStream is; - long bytes = 0; - - public CountInputStream(InputStream is) { - this.is = is; - } + InputStream is; + long bytes = 0; - @Override - public int available() throws IOException { - return is.available(); - } + public CountInputStream(InputStream is) { + this.is = is; + } - @Override - public void close() throws IOException { - is.close(); - } + @Override + public int available() throws IOException { + return is.available(); + } - @Override - public void mark(int readlimit) { - is.mark(readlimit); - } + @Override + public void close() throws IOException { + is.close(); + } - @Override - public boolean markSupported() { - return is.markSupported(); - } + @Override + public void mark(int readlimit) { + is.mark(readlimit); + } - @Override - public int read() throws IOException { - bytes++; - return is.read(); - } + @Override + public boolean markSupported() { + return is.markSupported(); + } - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - bytes += length; - return is.read(buffer, offset, length); - } + @Override + public int read() throws IOException { + bytes++; + return is.read(); + } - @Override - public int read(byte[] buffer) throws IOException { - bytes += buffer.length; - return is.read(buffer); - } + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + bytes += length; + return is.read(buffer, offset, length); + } - @Override - public synchronized void reset() throws IOException { - is.reset(); - } + @Override + public int read(byte[] buffer) throws IOException { + bytes += buffer.length; + return is.read(buffer); + } - @Override - public long skip(long byteCount) throws IOException { - bytes += byteCount; - return is.skip(byteCount); - } + @Override + public synchronized void reset() throws IOException { + is.reset(); + } + + @Override + public long skip(long byteCount) throws IOException { + bytes += byteCount; + return is.skip(byteCount); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/CountOutputStream.java b/app/src/main/java/com/kunzisoft/keepass/stream/CountOutputStream.java index 945874a1c..0ca655b05 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/CountOutputStream.java +++ b/app/src/main/java/com/kunzisoft/keepass/stream/CountOutputStream.java @@ -1,6 +1,6 @@ /* * 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 @@ -23,40 +23,40 @@ import java.io.IOException; import java.io.OutputStream; public class CountOutputStream extends OutputStream { - OutputStream os; - long bytes = 0; - - public CountOutputStream(OutputStream os) { - this.os = os; - } + OutputStream os; + long bytes = 0; + + public CountOutputStream(OutputStream os) { + this.os = os; + } - @Override - public void close() throws IOException { - os.close(); - } + @Override + public void close() throws IOException { + os.close(); + } - @Override - public void flush() throws IOException { - os.flush(); - } + @Override + public void flush() throws IOException { + os.flush(); + } - @Override - public void write(byte[] buffer, int offset, int count) throws IOException { - bytes += count; - os.write(buffer, offset, count); - } + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + bytes += count; + os.write(buffer, offset, count); + } - @Override - public void write(byte[] buffer) throws IOException { - bytes += buffer.length; - os.write(buffer); - } + @Override + public void write(byte[] buffer) throws IOException { + bytes += buffer.length; + os.write(buffer); + } - @Override - public void write(int oneByte) throws IOException { - bytes++; - os.write(oneByte); - } + @Override + public void write(int oneByte) throws IOException { + bytes++; + os.write(oneByte); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/HashedBlockInputStream.java b/app/src/main/java/com/kunzisoft/keepass/stream/HashedBlockInputStream.java index c44a2b6bd..9adafcbfc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/HashedBlockInputStream.java +++ b/app/src/main/java/com/kunzisoft/keepass/stream/HashedBlockInputStream.java @@ -1,6 +1,6 @@ /* * 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 @@ -29,137 +29,137 @@ import java.util.Arrays; public class HashedBlockInputStream extends InputStream { - - private final static int HASH_SIZE = 32; - private LEDataInputStream baseStream; - private int bufferPos = 0; - private byte[] buffer = new byte[0]; - private long bufferIndex = 0; - private boolean atEnd = false; - - - @Override - public int read(byte[] b) throws IOException { - return read(b, 0, b.length); - } + private final static int HASH_SIZE = 32; - public HashedBlockInputStream(InputStream is) { - baseStream = new LEDataInputStream(is); - } - - @Override - public int read(byte[] b, int offset, int length) throws IOException { - if ( atEnd ) return -1; - - int remaining = length; - - while ( remaining > 0 ) { - if ( bufferPos == buffer.length ) { - // Get more from the source into the buffer - if ( ! ReadHashedBlock() ) { - return length - remaining; - } - - } + private LEDataInputStream baseStream; + private int bufferPos = 0; + private byte[] buffer = new byte[0]; + private long bufferIndex = 0; + private boolean atEnd = false; - // Copy from buffer out - int copyLen = Math.min(buffer.length - bufferPos, remaining); - - System.arraycopy(buffer, bufferPos, b, offset, copyLen); - - offset += copyLen; - bufferPos += copyLen; - - remaining -= copyLen; - } - - return length; - } - /** - * @return false, when the end of the source stream is reached - * @throws IOException - */ - private boolean ReadHashedBlock() throws IOException { - if ( atEnd ) return false; - - bufferPos = 0; - - long index = baseStream.readUInt(); - if ( index != bufferIndex ) { - throw new IOException("Invalid data format"); - } - bufferIndex++; - - byte[] storedHash = baseStream.readBytes(32); - if ( storedHash == null || storedHash.length != HASH_SIZE) { - throw new IOException("Invalid data format"); - } - - int bufferSize = LEDataInputStream.readInt(baseStream); - if ( bufferSize < 0 ) { - throw new IOException("Invalid data format"); - } - - if ( bufferSize == 0 ) { - for (int hash = 0; hash < HASH_SIZE; hash++) { - if ( storedHash[hash] != 0 ) { - throw new IOException("Invalid data format"); - } - } - - atEnd = true; - buffer = new byte[0]; - return false; - } - - buffer = baseStream.readBytes(bufferSize); - if ( buffer == null || buffer.length != bufferSize ) { - throw new IOException("Invalid data format"); - } - - MessageDigest md = null; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IOException("SHA-256 not implemented here."); - } - - byte[] computedHash = md.digest(buffer); - if ( computedHash == null || computedHash.length != HASH_SIZE ) { - throw new IOException("Hash wrong size"); - } - - if ( ! Arrays.equals(storedHash, computedHash) ) { - throw new IOException("Hashes didn't match."); - } + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } - return true; - } + public HashedBlockInputStream(InputStream is) { + baseStream = new LEDataInputStream(is); + } - @Override - public long skip(long n) throws IOException { - return 0; - } + @Override + public int read(byte[] b, int offset, int length) throws IOException { + if ( atEnd ) return -1; - @Override - public int read() throws IOException { - if ( atEnd ) return -1; - - if ( bufferPos == buffer.length ) { - if ( ! ReadHashedBlock() ) return -1; - } - - int output = Types.readUByte(buffer, bufferPos); - bufferPos++; - - return output; - } + int remaining = length; - @Override - public void close() throws IOException { - baseStream.close(); - } + while ( remaining > 0 ) { + if ( bufferPos == buffer.length ) { + // Get more from the source into the buffer + if ( ! ReadHashedBlock() ) { + return length - remaining; + } + + } + + // Copy from buffer out + int copyLen = Math.min(buffer.length - bufferPos, remaining); + + System.arraycopy(buffer, bufferPos, b, offset, copyLen); + + offset += copyLen; + bufferPos += copyLen; + + remaining -= copyLen; + } + + return length; + } + + /** + * @return false, when the end of the source stream is reached + * @throws IOException + */ + private boolean ReadHashedBlock() throws IOException { + if ( atEnd ) return false; + + bufferPos = 0; + + long index = baseStream.readUInt(); + if ( index != bufferIndex ) { + throw new IOException("Invalid data format"); + } + bufferIndex++; + + byte[] storedHash = baseStream.readBytes(32); + if ( storedHash == null || storedHash.length != HASH_SIZE) { + throw new IOException("Invalid data format"); + } + + int bufferSize = LEDataInputStream.readInt(baseStream); + if ( bufferSize < 0 ) { + throw new IOException("Invalid data format"); + } + + if ( bufferSize == 0 ) { + for (int hash = 0; hash < HASH_SIZE; hash++) { + if ( storedHash[hash] != 0 ) { + throw new IOException("Invalid data format"); + } + } + + atEnd = true; + buffer = new byte[0]; + return false; + } + + buffer = baseStream.readBytes(bufferSize); + if ( buffer == null || buffer.length != bufferSize ) { + throw new IOException("Invalid data format"); + } + + MessageDigest md = null; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 not implemented here."); + } + + byte[] computedHash = md.digest(buffer); + if ( computedHash == null || computedHash.length != HASH_SIZE ) { + throw new IOException("Hash wrong size"); + } + + if ( ! Arrays.equals(storedHash, computedHash) ) { + throw new IOException("Hashes didn't match."); + } + + return true; + } + + @Override + public long skip(long n) throws IOException { + return 0; + } + + @Override + public int read() throws IOException { + if ( atEnd ) return -1; + + if ( bufferPos == buffer.length ) { + if ( ! ReadHashedBlock() ) return -1; + } + + int output = Types.readUByte(buffer, bufferPos); + bufferPos++; + + return output; + } + + @Override + public void close() throws IOException { + baseStream.close(); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/HashedBlockOutputStream.java b/app/src/main/java/com/kunzisoft/keepass/stream/HashedBlockOutputStream.java index b1fce5c47..ab88a4a5d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/HashedBlockOutputStream.java +++ b/app/src/main/java/com/kunzisoft/keepass/stream/HashedBlockOutputStream.java @@ -1,6 +1,6 @@ /* * 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 @@ -26,90 +26,90 @@ import java.security.NoSuchAlgorithmException; public class HashedBlockOutputStream extends OutputStream { - private final static int DEFAULT_BUFFER_SIZE = 1024 * 1024; - - private LEDataOutputStream baseStream; - private int bufferPos = 0; - private byte[] buffer; - private long bufferIndex = 0; - - public HashedBlockOutputStream(OutputStream os) { - init(os, DEFAULT_BUFFER_SIZE); - } - - public HashedBlockOutputStream(OutputStream os, int bufferSize) { - if ( bufferSize <= 0 ) { - bufferSize = DEFAULT_BUFFER_SIZE; - } - - init(os, bufferSize); - } - - private void init(OutputStream os, int bufferSize) { - baseStream = new LEDataOutputStream(os); - buffer = new byte[bufferSize]; - - } + private final static int DEFAULT_BUFFER_SIZE = 1024 * 1024; - @Override - public void write(int oneByte) throws IOException { - byte[] buf = new byte[1]; - buf[0] = (byte)oneByte; - write(buf, 0, 1); - } + private LEDataOutputStream baseStream; + private int bufferPos = 0; + private byte[] buffer; + private long bufferIndex = 0; - @Override - public void close() throws IOException { - if ( bufferPos != 0 ) { - // Write remaining buffered amount - WriteHashedBlock(); - } - - // Write terminating block - WriteHashedBlock(); - - flush(); - baseStream.close(); - } + public HashedBlockOutputStream(OutputStream os) { + init(os, DEFAULT_BUFFER_SIZE); + } - @Override - public void flush() throws IOException { - baseStream.flush(); - } + public HashedBlockOutputStream(OutputStream os, int bufferSize) { + if ( bufferSize <= 0 ) { + bufferSize = DEFAULT_BUFFER_SIZE; + } - @Override - public void write(byte[] b, int offset, int count) throws IOException { - while ( count > 0 ) { - if ( bufferPos == buffer.length ) { - WriteHashedBlock(); - } - - int copyLen = Math.min(buffer.length - bufferPos, count); - - System.arraycopy(b, offset, buffer, bufferPos, copyLen); - - offset += copyLen; - bufferPos += copyLen; - - count -= copyLen; - } - } + init(os, bufferSize); + } - private void WriteHashedBlock() throws IOException { - baseStream.writeUInt(bufferIndex); - bufferIndex++; - - if ( bufferPos > 0 ) { - MessageDigest md = null; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IOException("SHA-256 not implemented here."); - } - - byte[] hash; - md.update(buffer, 0, bufferPos); - hash = md.digest(); + private void init(OutputStream os, int bufferSize) { + baseStream = new LEDataOutputStream(os); + buffer = new byte[bufferSize]; + + } + + @Override + public void write(int oneByte) throws IOException { + byte[] buf = new byte[1]; + buf[0] = (byte)oneByte; + write(buf, 0, 1); + } + + @Override + public void close() throws IOException { + if ( bufferPos != 0 ) { + // Write remaining buffered amount + WriteHashedBlock(); + } + + // Write terminating block + WriteHashedBlock(); + + flush(); + baseStream.close(); + } + + @Override + public void flush() throws IOException { + baseStream.flush(); + } + + @Override + public void write(byte[] b, int offset, int count) throws IOException { + while ( count > 0 ) { + if ( bufferPos == buffer.length ) { + WriteHashedBlock(); + } + + int copyLen = Math.min(buffer.length - bufferPos, count); + + System.arraycopy(b, offset, buffer, bufferPos, copyLen); + + offset += copyLen; + bufferPos += copyLen; + + count -= copyLen; + } + } + + private void WriteHashedBlock() throws IOException { + baseStream.writeUInt(bufferIndex); + bufferIndex++; + + if ( bufferPos > 0 ) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 not implemented here."); + } + + byte[] hash; + md.update(buffer, 0, bufferPos); + hash = md.digest(); /* if ( bufferPos == buffer.length) { hash = md.digest(buffer); @@ -119,30 +119,30 @@ public class HashedBlockOutputStream extends OutputStream { hash = md.digest(b); } */ - - baseStream.write(hash); - } else { - // Write 32-bits of zeros - baseStream.writeLong(0L); - baseStream.writeLong(0L); - baseStream.writeLong(0L); - baseStream.writeLong(0L); - } - - baseStream.writeInt(bufferPos); - - if ( bufferPos > 0 ) { - baseStream.write(buffer, 0, bufferPos); - } - - bufferPos = 0; - - } + baseStream.write(hash); - @Override - public void write(byte[] buffer) throws IOException { - write(buffer, 0, buffer.length); - } + } else { + // Write 32-bits of zeros + baseStream.writeLong(0L); + baseStream.writeLong(0L); + baseStream.writeLong(0L); + baseStream.writeLong(0L); + } + + baseStream.writeInt(bufferPos); + + if ( bufferPos > 0 ) { + baseStream.write(buffer, 0, bufferPos); + } + + bufferPos = 0; + + } + + @Override + public void write(byte[] buffer) throws IOException { + write(buffer, 0, buffer.length); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/LEDataInputStream.java b/app/src/main/java/com/kunzisoft/keepass/stream/LEDataInputStream.java index 1af163754..fb3ef1615 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/LEDataInputStream.java +++ b/app/src/main/java/com/kunzisoft/keepass/stream/LEDataInputStream.java @@ -1,6 +1,6 @@ /* * 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 @@ -30,100 +30,100 @@ import java.util.Arrays; */ public class LEDataInputStream extends InputStream { - private static final long INT_TO_LONG_MASK = 0xffffffffL; - - private InputStream baseStream; + private static final long INT_TO_LONG_MASK = 0xffffffffL; - public LEDataInputStream(InputStream in) { - baseStream = in; - } - - /** Read a 32-bit value and return it as a long, so that it can - * be interpreted as an unsigned integer. - * @return - * @throws IOException - */ - public long readUInt() throws IOException { - return readUInt(baseStream); - } - - public int readInt() throws IOException { - return readInt(baseStream); - } - - public long readLong() throws IOException { - byte[] buf = readBytes(8); - - return readLong(buf, 0); - } - - @Override - public int available() throws IOException { - return baseStream.available(); - } + private InputStream baseStream; - @Override - public void close() throws IOException { - baseStream.close(); - } + public LEDataInputStream(InputStream in) { + baseStream = in; + } - @Override - public void mark(int readlimit) { - baseStream.mark(readlimit); - } + /** Read a 32-bit value and return it as a long, so that it can + * be interpreted as an unsigned integer. + * @return + * @throws IOException + */ + public long readUInt() throws IOException { + return readUInt(baseStream); + } - @Override - public boolean markSupported() { - return baseStream.markSupported(); - } + public int readInt() throws IOException { + return readInt(baseStream); + } - @Override - public int read() throws IOException { - return baseStream.read(); - } + public long readLong() throws IOException { + byte[] buf = readBytes(8); - @Override - public int read(byte[] b, int offset, int length) throws IOException { - return baseStream.read(b, offset, length); - } + return readLong(buf, 0); + } - @Override - public int read(byte[] b) throws IOException { - // TODO Auto-generated method stub - return super.read(b); - } + @Override + public int available() throws IOException { + return baseStream.available(); + } - @Override - public synchronized void reset() throws IOException { - baseStream.reset(); - } + @Override + public void close() throws IOException { + baseStream.close(); + } - @Override - public long skip(long n) throws IOException { - return baseStream.skip(n); - } + @Override + public void mark(int readlimit) { + baseStream.mark(readlimit); + } - public byte[] readBytes(int length) throws IOException { - // TODO Exception max length < buffer size - byte[] buf = new byte[length]; + @Override + public boolean markSupported() { + return baseStream.markSupported(); + } - int count = 0; - while ( count < length ) { - int read = read(buf, count, length - count); - - // Reached end - if ( read == -1 ) { - // Stop early - byte[] early = new byte[count]; - System.arraycopy(buf, 0, early, 0, count); - return early; - } - - count += read; - } - - return buf; - } + @Override + public int read() throws IOException { + return baseStream.read(); + } + + @Override + public int read(byte[] b, int offset, int length) throws IOException { + return baseStream.read(b, offset, length); + } + + @Override + public int read(byte[] b) throws IOException { + // TODO Auto-generated method stub + return super.read(b); + } + + @Override + public synchronized void reset() throws IOException { + baseStream.reset(); + } + + @Override + public long skip(long n) throws IOException { + return baseStream.skip(n); + } + + public byte[] readBytes(int length) throws IOException { + // TODO Exception max length < buffer size + byte[] buf = new byte[length]; + + int count = 0; + while ( count < length ) { + int read = read(buf, count, length - count); + + // Reached end + if ( read == -1 ) { + // Stop early + byte[] early = new byte[count]; + System.arraycopy(buf, 0, early, 0, count); + return early; + } + + count += read; + } + + return buf; + } public void readBytes(int length, ActionReadBytes actionReadBytes) throws IOException { int bufferSize = 256 * 3; // TODO Buffer size @@ -152,62 +152,62 @@ public class LEDataInputStream extends InputStream { } } - public static int readUShort(InputStream is) throws IOException { - byte[] buf = new byte[2]; - - is.read(buf, 0, 2); - - return readUShort(buf, 0); - } - - public int readUShort() throws IOException { - return readUShort(baseStream); - } + public static int readUShort(InputStream is) throws IOException { + byte[] buf = new byte[2]; - /** - * Read an unsigned 16-bit value. - * - * @param buf - * @param offset - * @return - */ - public static int readUShort( byte[] buf, int offset ) { - return (buf[offset] & 0xFF) + ((buf[offset + 1] & 0xFF) << 8); - } + is.read(buf, 0, 2); - public static long readLong( byte buf[], int offset ) { - return ((long)buf[offset] & 0xFF) + (((long)buf[offset + 1] & 0xFF) << 8) - + (((long)buf[offset + 2] & 0xFF) << 16) + (((long)buf[offset + 3] & 0xFF) << 24) - + (((long)buf[offset + 4] & 0xFF) << 32) + (((long)buf[offset + 5] & 0xFF) << 40) - + (((long)buf[offset + 6] & 0xFF) << 48) + (((long)buf[offset + 7] & 0xFF) << 56); - } + return readUShort(buf, 0); + } - public static long readUInt( byte buf[], int offset ) { - return (readInt(buf, offset) & INT_TO_LONG_MASK); - } + public int readUShort() throws IOException { + return readUShort(baseStream); + } - public static int readInt(InputStream is) throws IOException { - byte[] buf = new byte[4]; - - is.read(buf, 0, 4); - - return readInt(buf, 0); - } + /** + * Read an unsigned 16-bit value. + * + * @param buf + * @param offset + * @return + */ + public static int readUShort( byte[] buf, int offset ) { + return (buf[offset] & 0xFF) + ((buf[offset + 1] & 0xFF) << 8); + } - public static long readUInt(InputStream is) throws IOException { - return (readInt(is) & INT_TO_LONG_MASK); - } + public static long readLong( byte buf[], int offset ) { + return ((long)buf[offset] & 0xFF) + (((long)buf[offset + 1] & 0xFF) << 8) + + (((long)buf[offset + 2] & 0xFF) << 16) + (((long)buf[offset + 3] & 0xFF) << 24) + + (((long)buf[offset + 4] & 0xFF) << 32) + (((long)buf[offset + 5] & 0xFF) << 40) + + (((long)buf[offset + 6] & 0xFF) << 48) + (((long)buf[offset + 7] & 0xFF) << 56); + } - /** - * Read a 32-bit value. - * - * @param buf - * @param offset - * @return - */ - public static int readInt( byte buf[], int offset ) { - return (buf[offset] & 0xFF) + ((buf[offset + 1] & 0xFF) << 8) + ((buf[offset + 2] & 0xFF) << 16) - + ((buf[offset + 3] & 0xFF) << 24); - } + public static long readUInt( byte buf[], int offset ) { + return (readInt(buf, offset) & INT_TO_LONG_MASK); + } + + public static int readInt(InputStream is) throws IOException { + byte[] buf = new byte[4]; + + is.read(buf, 0, 4); + + return readInt(buf, 0); + } + + public static long readUInt(InputStream is) throws IOException { + return (readInt(is) & INT_TO_LONG_MASK); + } + + /** + * Read a 32-bit value. + * + * @param buf + * @param offset + * @return + */ + public static int readInt( byte buf[], int offset ) { + return (buf[offset] & 0xFF) + ((buf[offset + 1] & 0xFF) << 8) + ((buf[offset + 2] & 0xFF) << 16) + + ((buf[offset + 3] & 0xFF) << 24); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/LEDataOutputStream.java b/app/src/main/java/com/kunzisoft/keepass/stream/LEDataOutputStream.java index 82a02fbe6..bb4cf26eb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/LEDataOutputStream.java +++ b/app/src/main/java/com/kunzisoft/keepass/stream/LEDataOutputStream.java @@ -1,6 +1,6 @@ /* * 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 @@ -29,116 +29,116 @@ import java.io.OutputStream; */ public class LEDataOutputStream extends OutputStream { - private OutputStream baseStream; - - public LEDataOutputStream(OutputStream out) { - baseStream = out; - } - - public void writeUInt(long uint) throws IOException { - baseStream.write(LEDataOutputStream.writeIntBuf((int) uint)); - } + private OutputStream baseStream; - @Override - public void close() throws IOException { - baseStream.close(); - } + public LEDataOutputStream(OutputStream out) { + baseStream = out; + } - @Override - public void flush() throws IOException { - baseStream.flush(); - } + public void writeUInt(long uint) throws IOException { + baseStream.write(LEDataOutputStream.writeIntBuf((int) uint)); + } - @Override - public void write(byte[] buffer, int offset, int count) throws IOException { - baseStream.write(buffer, offset, count); - } + @Override + public void close() throws IOException { + baseStream.close(); + } - @Override - public void write(byte[] buffer) throws IOException { - baseStream.write(buffer); - } + @Override + public void flush() throws IOException { + baseStream.flush(); + } - @Override - public void write(int oneByte) throws IOException { - baseStream.write(oneByte); - } - - public void writeLong(long val) throws IOException { - byte[] buf = new byte[8]; - - writeLong(val, buf, 0); - baseStream.write(buf); - } - - public void writeInt(int val) throws IOException { - byte[] buf = new byte[4]; - writeInt(val, buf, 0); - - baseStream.write(buf); - } - - public void writeUShort(int val) throws IOException { - byte[] buf = new byte[2]; - writeUShort(val, buf, 0); - baseStream.write(buf); - } + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + baseStream.write(buffer, offset, count); + } - public static byte[] writeIntBuf(int val) { - byte[] buf = new byte[4]; - writeInt(val, buf, 0); - - return buf; - } + @Override + public void write(byte[] buffer) throws IOException { + baseStream.write(buffer); + } - public static byte[] writeUShortBuf(int val) { - byte[] buf = new byte[2]; - - writeUShort(val, buf, 0); - - return buf; - } + @Override + public void write(int oneByte) throws IOException { + baseStream.write(oneByte); + } - /** Write an unsigned 16-bit value - * - * @param val - * @param buf - * @param offset - */ - public static void writeUShort(int val, byte[] buf, int offset) { - buf[offset + 0] = (byte)(val & 0x00FF); - buf[offset + 1] = (byte)((val & 0xFF00) >> 8); - } + public void writeLong(long val) throws IOException { + byte[] buf = new byte[8]; - /** - * Write a 32-bit value. - * - * @param val - * @param buf - * @param offset - */ - public static void writeInt( int val, byte[] buf, int offset ) { - buf[offset + 0] = (byte)(val & 0xFF); - buf[offset + 1] = (byte)((val >>> 8) & 0xFF); - buf[offset + 2] = (byte)((val >>> 16) & 0xFF); - buf[offset + 3] = (byte)((val >>> 24) & 0xFF); - } - - public static byte[] writeLongBuf(long val) { - byte[] buf = new byte[8]; - writeLong(val, buf, 0); - return buf; - } + writeLong(val, buf, 0); + baseStream.write(buf); + } - public static void writeLong( long val, byte[] buf, int offset ) { - buf[offset + 0] = (byte)(val & 0xFF); - buf[offset + 1] = (byte)((val >>> 8) & 0xFF); - buf[offset + 2] = (byte)((val >>> 16) & 0xFF); - buf[offset + 3] = (byte)((val >>> 24) & 0xFF); - buf[offset + 4] = (byte)((val >>> 32) & 0xFF); - buf[offset + 5] = (byte)((val >>> 40) & 0xFF); - buf[offset + 6] = (byte)((val >>> 48) & 0xFF); - buf[offset + 7] = (byte)((val >>> 56) & 0xFF); - } + public void writeInt(int val) throws IOException { + byte[] buf = new byte[4]; + writeInt(val, buf, 0); + + baseStream.write(buf); + } + + public void writeUShort(int val) throws IOException { + byte[] buf = new byte[2]; + writeUShort(val, buf, 0); + baseStream.write(buf); + } + + public static byte[] writeIntBuf(int val) { + byte[] buf = new byte[4]; + writeInt(val, buf, 0); + + return buf; + } + + public static byte[] writeUShortBuf(int val) { + byte[] buf = new byte[2]; + + writeUShort(val, buf, 0); + + return buf; + } + + /** Write an unsigned 16-bit value + * + * @param val + * @param buf + * @param offset + */ + public static void writeUShort(int val, byte[] buf, int offset) { + buf[offset + 0] = (byte)(val & 0x00FF); + buf[offset + 1] = (byte)((val & 0xFF00) >> 8); + } + + /** + * Write a 32-bit value. + * + * @param val + * @param buf + * @param offset + */ + public static void writeInt( int val, byte[] buf, int offset ) { + buf[offset + 0] = (byte)(val & 0xFF); + buf[offset + 1] = (byte)((val >>> 8) & 0xFF); + buf[offset + 2] = (byte)((val >>> 16) & 0xFF); + buf[offset + 3] = (byte)((val >>> 24) & 0xFF); + } + + public static byte[] writeLongBuf(long val) { + byte[] buf = new byte[8]; + writeLong(val, buf, 0); + return buf; + } + + public static void writeLong( long val, byte[] buf, int offset ) { + buf[offset + 0] = (byte)(val & 0xFF); + buf[offset + 1] = (byte)((val >>> 8) & 0xFF); + buf[offset + 2] = (byte)((val >>> 16) & 0xFF); + buf[offset + 3] = (byte)((val >>> 24) & 0xFF); + buf[offset + 4] = (byte)((val >>> 32) & 0xFF); + buf[offset + 5] = (byte)((val >>> 40) & 0xFF); + buf[offset + 6] = (byte)((val >>> 48) & 0xFF); + buf[offset + 7] = (byte)((val >>> 56) & 0xFF); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/NullOutputStream.java b/app/src/main/java/com/kunzisoft/keepass/stream/NullOutputStream.java index 0e1a70b66..12eef13e3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/NullOutputStream.java +++ b/app/src/main/java/com/kunzisoft/keepass/stream/NullOutputStream.java @@ -1,6 +1,6 @@ /* * 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 @@ -24,28 +24,28 @@ import java.io.OutputStream; public class NullOutputStream extends OutputStream { - @Override - public void close() throws IOException { - super.close(); - } + @Override + public void close() throws IOException { + super.close(); + } - @Override - public void flush() throws IOException { - super.flush(); - } + @Override + public void flush() throws IOException { + super.flush(); + } - @Override - public void write(byte[] buffer, int offset, int count) throws IOException { - super.write(buffer, offset, count); - } + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + super.write(buffer, offset, count); + } - @Override - public void write(byte[] buffer) throws IOException { - super.write(buffer); - } + @Override + public void write(byte[] buffer) throws IOException { + super.write(buffer); + } - @Override - public void write(int oneByte) throws IOException { - } + @Override + public void write(int oneByte) throws IOException { + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/RandomFileOutputStream.java b/app/src/main/java/com/kunzisoft/keepass/stream/RandomFileOutputStream.java index 220ba2e64..06ce4f594 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/RandomFileOutputStream.java +++ b/app/src/main/java/com/kunzisoft/keepass/stream/RandomFileOutputStream.java @@ -1,6 +1,6 @@ /* * 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 @@ -25,40 +25,40 @@ import java.io.RandomAccessFile; public class RandomFileOutputStream extends OutputStream { - RandomAccessFile mFile; - - RandomFileOutputStream(RandomAccessFile file) { - mFile = file; - } - - @Override - public void close() throws IOException { - super.close(); - - mFile.close(); - } + RandomAccessFile mFile; - @Override - public void write(byte[] buffer, int offset, int count) throws IOException { - super.write(buffer, offset, count); - - mFile.write(buffer, offset, count); - } + RandomFileOutputStream(RandomAccessFile file) { + mFile = file; + } - @Override - public void write(byte[] buffer) throws IOException { - super.write(buffer); - - mFile.write(buffer); - } + @Override + public void close() throws IOException { + super.close(); - @Override - public void write(int oneByte) throws IOException { - mFile.write(oneByte); - } - - public void seek(long pos) throws IOException { - mFile.seek(pos); - } + mFile.close(); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + super.write(buffer, offset, count); + + mFile.write(buffer, offset, count); + } + + @Override + public void write(byte[] buffer) throws IOException { + super.write(buffer); + + mFile.write(buffer); + } + + @Override + public void write(int oneByte) throws IOException { + mFile.write(oneByte); + } + + public void seek(long pos) throws IOException { + mFile.seek(pos); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/EmptyUtils.java b/app/src/main/java/com/kunzisoft/keepass/utils/EmptyUtils.java index 2adb5f012..3ea26f3cd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/EmptyUtils.java +++ b/app/src/main/java/com/kunzisoft/keepass/utils/EmptyUtils.java @@ -1,6 +1,6 @@ /* * 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 @@ -24,19 +24,19 @@ import android.net.Uri; import com.kunzisoft.keepass.database.element.PwDate; public class EmptyUtils { - public static boolean isNullOrEmpty(String str) { - return (str == null) || (str.length() == 0); - } - - public static boolean isNullOrEmpty(byte[] buf) { - return (buf == null) || (buf.length == 0); - } - - public static boolean isNullOrEmpty(PwDate date) { - return (date == null) || date.equals(PwDate.DEFAULT_PWDATE); - } + public static boolean isNullOrEmpty(String str) { + return (str == null) || (str.length() == 0); + } - public static boolean isNullOrEmpty(Uri uri) { - return (uri==null) || (uri.toString().length() == 0); - } + public static boolean isNullOrEmpty(byte[] buf) { + return (buf == null) || (buf.length == 0); + } + + public static boolean isNullOrEmpty(PwDate date) { + return (date == null) || date.equals(PwDate.DEFAULT_PWDATE); + } + + public static boolean isNullOrEmpty(Uri uri) { + return (uri==null) || (uri.toString().length() == 0); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/Interaction.java b/app/src/main/java/com/kunzisoft/keepass/utils/Interaction.java index f498c4e05..2b5a51556 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/Interaction.java +++ b/app/src/main/java/com/kunzisoft/keepass/utils/Interaction.java @@ -1,6 +1,6 @@ /* * 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 @@ -27,24 +27,24 @@ import android.content.pm.ResolveInfo; import java.util.List; public class Interaction { - /** - * Indicates whether the specified action can be used as an intent. This - * method queries the package manager for installed packages that can - * respond to an intent with the specified action. If no suitable package is - * found, this method returns false. - * - * @param context The application's environment. - * @param action The Intent action to check for availability. - * - * @return True if an Intent with the specified action can be sent and - * responded to, false otherwise. - */ - public static boolean isIntentAvailable(Context context, String action) { - final PackageManager packageManager = context.getPackageManager(); - final Intent intent = new Intent(action); - List list = - packageManager.queryIntentActivities(intent, - PackageManager.MATCH_DEFAULT_ONLY); - return list.size() > 0; - } + /** + * Indicates whether the specified action can be used as an intent. This + * method queries the package manager for installed packages that can + * respond to an intent with the specified action. If no suitable package is + * found, this method returns false. + * + * @param context The application's environment. + * @param action The Intent action to check for availability. + * + * @return True if an Intent with the specified action can be sent and + * responded to, false otherwise. + */ + public static boolean isIntentAvailable(Context context, String action) { + final PackageManager packageManager = context.getPackageManager(); + final Intent intent = new Intent(action); + List list = + packageManager.queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY); + return list.size() > 0; + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/SprEngineV4.java b/app/src/main/java/com/kunzisoft/keepass/utils/SprEngineV4.java index 656e8d166..5f9fa0542 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/SprEngineV4.java +++ b/app/src/main/java/com/kunzisoft/keepass/utils/SprEngineV4.java @@ -1,6 +1,6 @@ /* * Copyright 2019 Jeremy Jamet / Kunzisoft. - * + * * This file is part of KeePass DX. * * KeePass DX is free software: you can redistribute it and/or modify @@ -30,221 +30,221 @@ import java.util.*; import java.util.Map.Entry; public class SprEngineV4 { - private static final int MAX_RECURSION_DEPTH = 12; - private static final String STR_REF_START = "{REF:"; - private static final String STR_REF_END = "}"; + private static final int MAX_RECURSION_DEPTH = 12; + private static final String STR_REF_START = "{REF:"; + private static final String STR_REF_END = "}"; - public class TargetResult { - public PwEntryV4 entry; - public char wanted; + public class TargetResult { + public PwEntryV4 entry; + public char wanted; - public TargetResult(PwEntryV4 entry, char wanted) { - this.entry = entry; - this.wanted = wanted; - } - } + public TargetResult(PwEntryV4 entry, char wanted) { + this.entry = entry; + this.wanted = wanted; + } + } - private class SprContextV4 { + private class SprContextV4 { - public PwDatabaseV4 db; - public PwEntryV4 entry; - public Map refsCache = new HashMap<>(); + public PwDatabaseV4 db; + public PwEntryV4 entry; + public Map refsCache = new HashMap<>(); - SprContextV4(PwDatabaseV4 db, PwEntryV4 entry) { - this.db = db; - this.entry = entry; - } + SprContextV4(PwDatabaseV4 db, PwEntryV4 entry) { + this.db = db; + this.entry = entry; + } - SprContextV4(SprContextV4 source) { - this.db = source.db; - this.entry = source.entry; - this.refsCache = source.refsCache; - } - } + SprContextV4(SprContextV4 source) { + this.db = source.db; + this.entry = source.entry; + this.refsCache = source.refsCache; + } + } - public String compile(String text, PwEntryV4 entry, PwDatabase database) { - SprContextV4 ctx = new SprContextV4((PwDatabaseV4)database, entry); + public String compile(String text, PwEntryV4 entry, PwDatabase database) { + SprContextV4 ctx = new SprContextV4((PwDatabaseV4)database, entry); - return compileInternal(text, ctx, 0); - } + return compileInternal(text, ctx, 0); + } - private String compileInternal(String text, SprContextV4 sprContextV4, int recursionLevel) { - if (text == null) { return ""; } - if (sprContextV4 == null) { return ""; } - if (recursionLevel >= MAX_RECURSION_DEPTH) { return ""; } + private String compileInternal(String text, SprContextV4 sprContextV4, int recursionLevel) { + if (text == null) { return ""; } + if (sprContextV4 == null) { return ""; } + if (recursionLevel >= MAX_RECURSION_DEPTH) { return ""; } - return fillRefPlaceholders(text, sprContextV4, recursionLevel); - } + return fillRefPlaceholders(text, sprContextV4, recursionLevel); + } - private String fillRefPlaceholders(String text, SprContextV4 contextV4, int recursionLevel) { + private String fillRefPlaceholders(String text, SprContextV4 contextV4, int recursionLevel) { - if (contextV4.db == null) { return text; } + if (contextV4.db == null) { return text; } - int offset = 0; - for (int i = 0; i < 20; ++i) { - text = fillRefsUsingCache(text, contextV4); + int offset = 0; + for (int i = 0; i < 20; ++i) { + text = fillRefsUsingCache(text, contextV4); - int start = StringUtil.INSTANCE.indexOfIgnoreCase(text, STR_REF_START, offset, Locale.ENGLISH); - if (start < 0) { break; } - int end = StringUtil.INSTANCE.indexOfIgnoreCase(text, STR_REF_END, start + 1, Locale.ENGLISH); - if (end <= start) { break; } + int start = StringUtil.INSTANCE.indexOfIgnoreCase(text, STR_REF_START, offset, Locale.ENGLISH); + if (start < 0) { break; } + int end = StringUtil.INSTANCE.indexOfIgnoreCase(text, STR_REF_END, start + 1, Locale.ENGLISH); + if (end <= start) { break; } - String fullRef = text.substring(start, end - start + 1); - TargetResult result = findRefTarget(fullRef, contextV4); + String fullRef = text.substring(start, end - start + 1); + TargetResult result = findRefTarget(fullRef, contextV4); - if (result != null) { + if (result != null) { PwEntryV4 found = result.entry; char wanted = result.wanted; if (found != null) { - String data; - switch (wanted) { - case 'T': - data = found.getTitle(); - break; - case 'U': - data = found.getUsername(); - break; - case 'A': - data = found.getUrl(); - break; - case 'P': - data = found.getPassword(); - break; - case 'N': - data = found.getNotes(); - break; - case 'I': - data = found.getNodeId().toString(); - break; - default: - offset = start + 1; - continue; - } + String data; + switch (wanted) { + case 'T': + data = found.getTitle(); + break; + case 'U': + data = found.getUsername(); + break; + case 'A': + data = found.getUrl(); + break; + case 'P': + data = found.getPassword(); + break; + case 'N': + data = found.getNotes(); + break; + case 'I': + data = found.getNodeId().toString(); + break; + default: + offset = start + 1; + continue; + } - SprContextV4 subCtx = new SprContextV4(contextV4); - subCtx.entry = found; + SprContextV4 subCtx = new SprContextV4(contextV4); + subCtx.entry = found; - String innerContent = compileInternal(data, subCtx, recursionLevel + 1); - addRefsToCache(fullRef, innerContent, contextV4); - text = fillRefsUsingCache(text, contextV4); + String innerContent = compileInternal(data, subCtx, recursionLevel + 1); + addRefsToCache(fullRef, innerContent, contextV4); + text = fillRefsUsingCache(text, contextV4); } else { - offset = start + 1; + offset = start + 1; } - } + } - } - - return text; - } - - private TargetResult findRefTarget(String fullRef, SprContextV4 contextV4) { - if (fullRef == null) { return null; } - - fullRef = fullRef.toUpperCase(Locale.ENGLISH); - if (!fullRef.startsWith(STR_REF_START) || !fullRef.endsWith(STR_REF_END)) { - return null; - } - - String ref = fullRef.substring(STR_REF_START.length(), fullRef.length() - STR_REF_START.length() - STR_REF_END.length()); - if (ref.length() <= 4) { return null; } - if (ref.charAt(1) != '@') { return null; } - if (ref.charAt(3) != ':') { return null; } - - char scan = Character.toUpperCase(ref.charAt(2)); - char wanted = Character.toUpperCase(ref.charAt(0)); - - SearchParametersV4 searchParametersV4 = new SearchParametersV4(); - searchParametersV4.setupNone(); - - searchParametersV4.setSearchString(ref.substring(4)); - if (scan == 'T') { searchParametersV4.setSearchInTitles(true); } - else if (scan == 'U') { searchParametersV4.setSearchInUserNames(true); } - else if (scan == 'A') { searchParametersV4.setSearchInUrls(true); } - else if (scan == 'P') { searchParametersV4.setSearchInPasswords(true); } - else if (scan == 'N') { searchParametersV4.setSearchInNotes(true); } - else if (scan == 'I') { searchParametersV4.setSearchInUUIDs(true); } - else if (scan == 'O') { searchParametersV4.setSearchInOther(true); } - else { return null; } - - List list = new ArrayList<>(); - // TODO type parameter - searchEntries(contextV4.db.getRootGroup(), searchParametersV4, list); - - if (list.size() > 0) { - return new TargetResult(list.get(0), wanted); } - return null; - } + return text; + } - private void addRefsToCache(String ref, String value, SprContextV4 ctx) { - if (ref == null) { return; } - if (value == null) { return; } - if (ctx == null) { return; } + private TargetResult findRefTarget(String fullRef, SprContextV4 contextV4) { + if (fullRef == null) { return null; } - if (!ctx.refsCache.containsKey(ref)) { - ctx.refsCache.put(ref, value); - } - } + fullRef = fullRef.toUpperCase(Locale.ENGLISH); + if (!fullRef.startsWith(STR_REF_START) || !fullRef.endsWith(STR_REF_END)) { + return null; + } - private String fillRefsUsingCache(String text, SprContextV4 sprContextV4) { - for (Entry entry : sprContextV4.refsCache.entrySet()) { - text = StringUtil.INSTANCE.replaceAllIgnoresCase(text, entry.getKey(), entry.getValue(), Locale.ENGLISH); - } + String ref = fullRef.substring(STR_REF_START.length(), fullRef.length() - STR_REF_START.length() - STR_REF_END.length()); + if (ref.length() <= 4) { return null; } + if (ref.charAt(1) != '@') { return null; } + if (ref.charAt(3) != ':') { return null; } - return text; - } + char scan = Character.toUpperCase(ref.charAt(2)); + char wanted = Character.toUpperCase(ref.charAt(0)); - private void searchEntries(PwGroupV4 root, SearchParametersV4 searchParametersV4, List listStorage) { - if (searchParametersV4 == null) { return; } - if (listStorage == null) { return; } + SearchParametersV4 searchParametersV4 = new SearchParametersV4(); + searchParametersV4.setupNone(); - List terms = StringUtil.INSTANCE.splitStringTerms(searchParametersV4.getSearchString()); - if (terms.size() <= 1 || searchParametersV4.getRegularExpression()) { - root.doForEachChild(new EntrySearchHandlerV4(searchParametersV4, listStorage), null); - return; - } + searchParametersV4.setSearchString(ref.substring(4)); + if (scan == 'T') { searchParametersV4.setSearchInTitles(true); } + else if (scan == 'U') { searchParametersV4.setSearchInUserNames(true); } + else if (scan == 'A') { searchParametersV4.setSearchInUrls(true); } + else if (scan == 'P') { searchParametersV4.setSearchInPasswords(true); } + else if (scan == 'N') { searchParametersV4.setSearchInNotes(true); } + else if (scan == 'I') { searchParametersV4.setSearchInUUIDs(true); } + else if (scan == 'O') { searchParametersV4.setSearchInOther(true); } + else { return null; } - // Search longest term first - Comparator stringLengthComparator = (lhs, rhs) -> lhs.length() - rhs.length(); - Collections.sort(terms, stringLengthComparator); + List list = new ArrayList<>(); + // TODO type parameter + searchEntries(contextV4.db.getRootGroup(), searchParametersV4, list); - String fullSearch = searchParametersV4.getSearchString(); - List childEntries = root.getChildEntries(); - for (int i = 0; i < terms.size(); i ++) { - List pgNew = new ArrayList<>(); + if (list.size() > 0) { + return new TargetResult(list.get(0), wanted); + } - searchParametersV4.setSearchString(terms.get(i)); + return null; + } - boolean negate = false; - if (searchParametersV4.getSearchString().startsWith("-")) { - searchParametersV4.setSearchString(searchParametersV4.getSearchString().substring(1)); - negate = searchParametersV4.getSearchString().length() > 0; - } + private void addRefsToCache(String ref, String value, SprContextV4 ctx) { + if (ref == null) { return; } + if (value == null) { return; } + if (ctx == null) { return; } - if (!root.doForEachChild(new EntrySearchHandlerV4(searchParametersV4, pgNew), null)) { - childEntries = null; - break; - } + if (!ctx.refsCache.containsKey(ref)) { + ctx.refsCache.put(ref, value); + } + } - List complement = new ArrayList<>(); - if (negate) { - for (PwEntryV4 entry: childEntries) { - if (!pgNew.contains(entry)) { - complement.add(entry); - } - } - childEntries = complement; - } - else { - childEntries = pgNew; - } - } + private String fillRefsUsingCache(String text, SprContextV4 sprContextV4) { + for (Entry entry : sprContextV4.refsCache.entrySet()) { + text = StringUtil.INSTANCE.replaceAllIgnoresCase(text, entry.getKey(), entry.getValue(), Locale.ENGLISH); + } - if (childEntries != null) { - listStorage.addAll(childEntries); - } - searchParametersV4.setSearchString(fullSearch); - } + return text; + } + + private void searchEntries(PwGroupV4 root, SearchParametersV4 searchParametersV4, List listStorage) { + if (searchParametersV4 == null) { return; } + if (listStorage == null) { return; } + + List terms = StringUtil.INSTANCE.splitStringTerms(searchParametersV4.getSearchString()); + if (terms.size() <= 1 || searchParametersV4.getRegularExpression()) { + root.doForEachChild(new EntrySearchHandlerV4(searchParametersV4, listStorage), null); + return; + } + + // Search longest term first + Comparator stringLengthComparator = (lhs, rhs) -> lhs.length() - rhs.length(); + Collections.sort(terms, stringLengthComparator); + + String fullSearch = searchParametersV4.getSearchString(); + List childEntries = root.getChildEntries(); + for (int i = 0; i < terms.size(); i ++) { + List pgNew = new ArrayList<>(); + + searchParametersV4.setSearchString(terms.get(i)); + + boolean negate = false; + if (searchParametersV4.getSearchString().startsWith("-")) { + searchParametersV4.setSearchString(searchParametersV4.getSearchString().substring(1)); + negate = searchParametersV4.getSearchString().length() > 0; + } + + if (!root.doForEachChild(new EntrySearchHandlerV4(searchParametersV4, pgNew), null)) { + childEntries = null; + break; + } + + List complement = new ArrayList<>(); + if (negate) { + for (PwEntryV4 entry: childEntries) { + if (!pgNew.contains(entry)) { + complement.add(entry); + } + } + childEntries = complement; + } + else { + childEntries = pgNew; + } + } + + if (childEntries != null) { + listStorage.addAll(childEntries); + } + searchParametersV4.setSearchString(fullSearch); + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/Types.java b/app/src/main/java/com/kunzisoft/keepass/utils/Types.java index 18271e031..71eaacf12 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/Types.java +++ b/app/src/main/java/com/kunzisoft/keepass/utils/Types.java @@ -52,131 +52,131 @@ import java.util.UUID; /** * Tools for slicing and dicing Java and KeePass data types. - * + * * @author Bill Zwicky */ public class Types { - public static long ULONG_MAX_VALUE = -1; + public static long ULONG_MAX_VALUE = -1; - /** Read an unsigned byte */ - public static int readUByte( byte[] buf, int offset ) { - return ((int)buf[offset] & 0xFF); - } + /** Read an unsigned byte */ + public static int readUByte( byte[] buf, int offset ) { + return ((int)buf[offset] & 0xFF); + } - /** Write an unsigned byte - * - * @param val - * @param buf - * @param offset - */ - public static void writeUByte(int val, byte[] buf, int offset) { - buf[offset] = (byte)(val & 0xFF); - } - - public static byte writeUByte(int val) { - byte[] buf = new byte[1]; - - writeUByte(val, buf, 0); - - return buf[0]; - } + /** Write an unsigned byte + * + * @param val + * @param buf + * @param offset + */ + public static void writeUByte(int val, byte[] buf, int offset) { + buf[offset] = (byte)(val & 0xFF); + } - /** - * Return len of null-terminated string (i.e. distance to null) - * within a byte buffer. - * - * @param buf - * @param offset - * @return - */ - public static int strlen( byte[] buf, int offset ) { - int len = 0; - while( buf[offset + len] != 0 ) - len++; - return len; - } + public static byte writeUByte(int val) { + byte[] buf = new byte[1]; + + writeUByte(val, buf, 0); + + return buf[0]; + } + + /** + * Return len of null-terminated string (i.e. distance to null) + * within a byte buffer. + * + * @param buf + * @param offset + * @return + */ + public static int strlen( byte[] buf, int offset ) { + int len = 0; + while( buf[offset + len] != 0 ) + len++; + return len; + } - /** - * Copy a sequence of bytes into a new array. - * - * @param b - source array - * @param offset - first byte - * @param len - number of bytes - * @return new byte[len] - */ - public static byte[] extract( byte[] b, int offset, int len ) { - byte[] b2 = new byte[len]; - System.arraycopy( b, offset, b2, 0, len ); - return b2; - } - - - private static final byte[] CRLFbuf = { 0x0D, 0x0A }; - private static final String CRLF = new String(CRLFbuf); - private static final String SEP = System.getProperty("line.separator"); - private static final boolean REPLACE = ! SEP.equals(CRLF); - - public static String readCString(byte[] buf, int offset) throws UnsupportedEncodingException { - String jstring = new String(buf, offset, strlen(buf, offset), "UTF-8"); - - if ( REPLACE ) { - jstring = jstring.replace(CRLF, SEP); - } - - return jstring; - } + /** + * Copy a sequence of bytes into a new array. + * + * @param b - source array + * @param offset - first byte + * @param len - number of bytes + * @return new byte[len] + */ + public static byte[] extract( byte[] b, int offset, int len ) { + byte[] b2 = new byte[len]; + System.arraycopy( b, offset, b2, 0, len ); + return b2; + } - public static int writeCString(String str, OutputStream os) throws IOException { - if ( str == null ) { - // Write out a null character - os.write(LEDataOutputStream.writeIntBuf(1)); - os.write(0x00); - return 0; - } - - if ( REPLACE ) { - str = str.replace(SEP, CRLF); - } - - byte[] initial = str.getBytes("UTF-8"); - - int length = initial.length+1; - os.write(LEDataOutputStream.writeIntBuf(length)); - os.write(initial); - os.write(0x00); - - return length; - } - - public static UUID bytestoUUID(byte[] buf) { - return bytestoUUID(buf, 0); - } - - public static UUID bytestoUUID(byte[] buf, int offset) { - long lsb = 0; - for (int i = 15; i >= 8; i--) { - lsb = (lsb << 8) | (buf[i + offset] & 0xff); - } - - long msb = 0; - for (int i = 7; i >= 0; i--) { - msb = (msb << 8) | (buf[i + offset] & 0xff); - } - return new UUID(msb, lsb); + private static final byte[] CRLFbuf = { 0x0D, 0x0A }; + private static final String CRLF = new String(CRLFbuf); + private static final String SEP = System.getProperty("line.separator"); + private static final boolean REPLACE = ! SEP.equals(CRLF); + + public static String readCString(byte[] buf, int offset) throws UnsupportedEncodingException { + String jstring = new String(buf, offset, strlen(buf, offset), "UTF-8"); + + if ( REPLACE ) { + jstring = jstring.replace(CRLF, SEP); + } + + return jstring; + } + + public static int writeCString(String str, OutputStream os) throws IOException { + if ( str == null ) { + // Write out a null character + os.write(LEDataOutputStream.writeIntBuf(1)); + os.write(0x00); + return 0; + } + + if ( REPLACE ) { + str = str.replace(SEP, CRLF); + } + + byte[] initial = str.getBytes("UTF-8"); + + int length = initial.length+1; + os.write(LEDataOutputStream.writeIntBuf(length)); + os.write(initial); + os.write(0x00); + + return length; + } + + public static UUID bytestoUUID(byte[] buf) { + return bytestoUUID(buf, 0); + } + + public static UUID bytestoUUID(byte[] buf, int offset) { + long lsb = 0; + for (int i = 15; i >= 8; i--) { + lsb = (lsb << 8) | (buf[i + offset] & 0xff); + } + + long msb = 0; + for (int i = 7; i >= 0; i--) { + msb = (msb << 8) | (buf[i + offset] & 0xff); + } + + return new UUID(msb, lsb); + + } + + public static byte[] UUIDtoBytes(UUID uuid) { + byte[] buf = new byte[16]; + + LEDataOutputStream.writeLong(uuid.getMostSignificantBits(), buf, 0); + LEDataOutputStream.writeLong(uuid.getLeastSignificantBits(), buf, 8); + + return buf; + } - } - - public static byte[] UUIDtoBytes(UUID uuid) { - byte[] buf = new byte[16]; - - LEDataOutputStream.writeLong(uuid.getMostSignificantBits(), buf, 0); - LEDataOutputStream.writeLong(uuid.getLeastSignificantBits(), buf, 8); - - return buf; - } - } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/Util.java b/app/src/main/java/com/kunzisoft/keepass/utils/Util.java index ae8c899f0..bffdec0c3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/Util.java +++ b/app/src/main/java/com/kunzisoft/keepass/utils/Util.java @@ -1,6 +1,6 @@ /* * 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 @@ -34,18 +34,18 @@ import com.kunzisoft.keepass.R; public class Util { - public static void gotoUrl(Context context, String url) throws ActivityNotFoundException { - if ( url != null && url.length() > 0 ) { - Uri uri = Uri.parse(url); - context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); - } - } - - public static void gotoUrl(Context context, int resId) throws ActivityNotFoundException { - gotoUrl(context, context.getString(resId)); - } + public static void gotoUrl(Context context, String url) throws ActivityNotFoundException { + if ( url != null && url.length() > 0 ) { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + } - /** + public static void gotoUrl(Context context, int resId) throws ActivityNotFoundException { + gotoUrl(context, context.getString(resId)); + } + + /** * Replace font by monospace, must be called after seText() */ public static void applyFontVisibilityTo(final Context context, final TextView textView) { @@ -56,28 +56,28 @@ public class Util { /** * Replace font by monospace, must be called after seText() */ - public static void applyFontVisibilityTo(final Context context, final EditText editText) { + public static void applyFontVisibilityTo(final Context context, final EditText editText) { applyFontVisibilityTo(context, (TextView) editText); - } + } - public static float getListTextDefaultSize(Context context) { - return Float.parseFloat(context.getString(R.string.list_size_default)); - } + public static float getListTextDefaultSize(Context context) { + return Float.parseFloat(context.getString(R.string.list_size_default)); + } public static void lockScreenOrientation(Activity activity) { - if (activity != null) { - int currentOrientation = activity.getResources().getConfiguration().orientation; - if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { + if (activity != null) { + int currentOrientation = activity.getResources().getConfiguration().orientation; + if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } else { + } else { activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - } - } - } + } + } + } public static void unlockScreenOrientation(Activity activity) { if (activity != null) { activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } - } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/UuidUtil.java b/app/src/main/java/com/kunzisoft/keepass/utils/UuidUtil.java index 0caa8b448..b61875e1a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/UuidUtil.java +++ b/app/src/main/java/com/kunzisoft/keepass/utils/UuidUtil.java @@ -1,6 +1,6 @@ /* * 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 @@ -22,44 +22,44 @@ package com.kunzisoft.keepass.utils; import java.util.UUID; public class UuidUtil { - public static String toHexString(UUID uuid) { - if (uuid == null) { return null; } - - byte[] buf = Types.UUIDtoBytes(uuid); - if (buf == null) { return null; } - - int len = buf.length; - if (len == 0) { return ""; } - - StringBuilder sb = new StringBuilder(); - - short bt; - char high, low; - for (int i = 0; i < len; i++) { - bt = (short)(buf[i] & 0xFF); - high = (char)(bt >>> 4); - - - low = (char)(bt & 0x0F); - - char h,l; - h = byteToChar(high); - l = byteToChar(low); + public static String toHexString(UUID uuid) { + if (uuid == null) { return null; } - sb.append(byteToChar(high)); - sb.append(byteToChar(low)); - } - - return sb.toString(); - } - - // Use short to represent unsigned byte - private static char byteToChar(char bt) { + byte[] buf = Types.UUIDtoBytes(uuid); + if (buf == null) { return null; } + + int len = buf.length; + if (len == 0) { return ""; } + + StringBuilder sb = new StringBuilder(); + + short bt; + char high, low; + for (int i = 0; i < len; i++) { + bt = (short)(buf[i] & 0xFF); + high = (char)(bt >>> 4); + + + low = (char)(bt & 0x0F); + + char h,l; + h = byteToChar(high); + l = byteToChar(low); + + sb.append(byteToChar(high)); + sb.append(byteToChar(low)); + } + + return sb.toString(); + } + + // Use short to represent unsigned byte + private static char byteToChar(char bt) { if (bt >= 10) { return (char)('A' + bt - 10); } else { return (char)('0' + bt); } - } + } } From cdf8baeb108f8b432a0bef419b9361156a38a09a Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 4 Jul 2019 15:25:47 +0200 Subject: [PATCH 02/24] Kotlinized PwDate --- .../tests/{PwDateTest.java => PwDateTest.kt} | 23 +- .../kunzisoft/keepass/tests/TypesTest.java | 4 +- .../keepass/activities/EntryActivity.java | 2 +- .../java/com/kunzisoft/keepass/app/App.java | 10 - .../keepass/database/SortNodeEnum.kt | 18 +- .../keepass/database/element/PwDate.java | 266 ---------------- .../keepass/database/element/PwDate.kt | 284 ++++++++++++++++++ .../keepass/database/element/PwEntryV4.kt | 2 +- .../keepass/database/element/PwNode.kt | 3 +- .../database/file/save/PwEntryOutputV3.kt | 8 +- .../database/file/save/PwGroupOutputV3.kt | 8 +- .../fileselect/FileSelectActivity.java | 2 +- .../keepass/password/PasswordActivity.java | 6 +- .../utils/{EmptyUtils.java => EmptyUtils.kt} | 24 +- .../com/kunzisoft/keepass/utils/UriUtil.java | 19 +- 15 files changed, 342 insertions(+), 337 deletions(-) rename app/src/androidTest/java/com/kunzisoft/keepass/tests/{PwDateTest.java => PwDateTest.kt} (59%) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt rename app/src/main/java/com/kunzisoft/keepass/utils/{EmptyUtils.java => EmptyUtils.kt} (57%) diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwDateTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwDateTest.kt similarity index 59% rename from app/src/androidTest/java/com/kunzisoft/keepass/tests/PwDateTest.java rename to app/src/androidTest/java/com/kunzisoft/keepass/tests/PwDateTest.kt index 492c4e5f2..05a2993c7 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwDateTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/PwDateTest.kt @@ -17,22 +17,21 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.tests; +package com.kunzisoft.keepass.tests -import junit.framework.TestCase; +import junit.framework.TestCase -import com.kunzisoft.keepass.database.element.PwDate; +import com.kunzisoft.keepass.database.element.PwDate +import org.junit.Assert -public class PwDateTest extends TestCase { - public void testDate() { - PwDate jDate = new PwDate(System.currentTimeMillis()); +class PwDateTest : TestCase() { - PwDate intermediate = new PwDate(jDate); - - PwDate cDate = new PwDate(intermediate.getCDate(), 0); - - assertTrue("jDate and intermediate not equal", jDate.equals(intermediate)); - assertTrue("jDate and cDate not equal", cDate.equals(jDate)); + fun testDate() { + val jDate = PwDate(System.currentTimeMillis()) + val intermediate = PwDate(jDate) + val cDate = PwDate(intermediate.byteArrayDate!!, 0) + Assert.assertTrue("jDate and intermediate not equal", jDate == intermediate) + Assert.assertTrue("jDate and cDate not equal", cDate == jDate) } } diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.java b/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.java index aef346519..66f792b8b 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.java +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/TypesTest.java @@ -170,9 +170,9 @@ public class TypesTest extends TestCase { Calendar expected = Calendar.getInstance(); expected.set(2008, 1, 2, 3, 4, 5); - byte[] buf = PwDate.writeTime(expected.getTime(), cal); + byte[] buf = PwDate.Companion.writeTime(expected.getTime(), cal); Calendar actual = Calendar.getInstance(); - actual.setTime(PwDate.readTime(buf, 0, cal)); + actual.setTime(PwDate.Companion.readTime(buf, 0, cal)); assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR)); assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH)); diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java index 4ee2698da..6a5c6d020 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java @@ -452,7 +452,7 @@ public class EntryActivity extends LockingHideActivity { gotoUrl.setVisible(false); } else { String url = mEntry.getUrl(); - if (EmptyUtils.isNullOrEmpty(url)) { + if (EmptyUtils.INSTANCE.isNullOrEmpty(url)) { // disable button if url is not available gotoUrl.setVisible(false); } diff --git a/app/src/main/java/com/kunzisoft/keepass/app/App.java b/app/src/main/java/com/kunzisoft/keepass/app/App.java index 2d888d74d..93a4aba37 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/App.java +++ b/app/src/main/java/com/kunzisoft/keepass/app/App.java @@ -26,11 +26,8 @@ import com.kunzisoft.keepass.database.element.Database; import com.kunzisoft.keepass.fileselect.RecentFileHistory; import com.kunzisoft.keepass.stylish.Stylish; -import java.util.Calendar; - public class App extends MultiDexApplication { private static Database db = null; - private static Calendar calendar = null; private static RecentFileHistory fileHistory = null; public static Database getDB() { @@ -48,13 +45,6 @@ public class App extends MultiDexApplication { db = d; } - public static Calendar getCalendar() { - if ( calendar == null ) { - calendar = Calendar.getInstance(); - } - return calendar; - } - @Override public void onCreate() { super.onCreate(); diff --git a/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt b/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt index 5afd0ea7e..22a6b2415 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt @@ -111,7 +111,7 @@ enum class SortNodeEnum { object1, object2, object1.creationTime.date - .compareTo(object2.creationTime.date)) + ?.compareTo(object2.creationTime.date) ?: 0) } } @@ -128,7 +128,7 @@ enum class SortNodeEnum { object1, object2, object1.lastModificationTime.date - .compareTo(object2.lastModificationTime.date)) + ?.compareTo(object2.lastModificationTime.date) ?: 0) } } @@ -145,7 +145,7 @@ enum class SortNodeEnum { object1, object2, object1.lastAccessTime.date - .compareTo(object2.lastAccessTime.date)) + ?.compareTo(object2.lastAccessTime.date) ?: 0) } } @@ -186,7 +186,7 @@ enum class SortNodeEnum { return 0 val groupCreationComp = object1.creationTime.date - .compareTo(object2.creationTime.date) + ?.compareTo(object2.creationTime.date) ?: 0 // If same creation, can be different return if (groupCreationComp == 0) { object1.hashCode() - object2.hashCode() @@ -205,7 +205,7 @@ enum class SortNodeEnum { return 0 val groupLastModificationComp = object1.lastModificationTime.date - .compareTo(object2.lastModificationTime.date) + ?.compareTo(object2.lastModificationTime.date) ?: 0 // If same creation, can be different return if (groupLastModificationComp == 0) { object1.hashCode() - object2.hashCode() @@ -224,7 +224,7 @@ enum class SortNodeEnum { return 0 val groupLastAccessComp = object1.lastAccessTime.date - .compareTo(object2.lastAccessTime.date) + ?.compareTo(object2.lastAccessTime.date) ?: 0 // If same creation, can be different return if (groupLastAccessComp == 0) { object1.hashCode() - object2.hashCode() @@ -261,7 +261,7 @@ enum class SortNodeEnum { return 0 val entryCreationComp = object1.creationTime.date - .compareTo(object2.creationTime.date) + ?.compareTo(object2.creationTime.date) ?: 0 // If same creation, can be different return if (entryCreationComp == 0) { object1.hashCode() - object2.hashCode() @@ -280,7 +280,7 @@ enum class SortNodeEnum { return 0 val entryLastModificationComp = object1.lastModificationTime.date - .compareTo(object2.lastModificationTime.date) + ?.compareTo(object2.lastModificationTime.date) ?: 0 // If same creation, can be different return if (entryLastModificationComp == 0) { object1.hashCode() - object2.hashCode() @@ -299,7 +299,7 @@ enum class SortNodeEnum { return 0 val entryLastAccessComp = object1.lastAccessTime.date - .compareTo(object2.lastAccessTime.date) + ?.compareTo(object2.lastAccessTime.date) ?: 0 // If same creation, can be different return if (entryLastAccessComp == 0) { object1.hashCode() - object2.hashCode() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.java b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.java deleted file mode 100644 index 8e535faad..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.java +++ /dev/null @@ -1,266 +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.database.element; - -import android.os.Parcel; -import android.os.Parcelable; - -import com.kunzisoft.keepass.app.App; -import com.kunzisoft.keepass.utils.Types; - -import java.util.Arrays; -import java.util.Calendar; -import java.util.Date; - -/** Converting from the C Date format to the Java data format is - * expensive when done for every record at once. I use this class to - * allow lazy conversions between the formats. - * @author bpellin - * - */ -public class PwDate implements Parcelable { - - private static final int DATE_SIZE = 5; - - private Date jDate = null; - private boolean jDateBuilt = false; - transient private byte[] cDate = null; - transient private boolean cDateBuilt = false; - - public static final Date NEVER_EXPIRE = getNeverExpire(); - public static final Date DEFAULT_DATE = getDefaultDate(); - - public static final PwDate PW_NEVER_EXPIRE = new PwDate(NEVER_EXPIRE); - public static final PwDate DEFAULT_PWDATE = new PwDate(DEFAULT_DATE); - - private static Date getDefaultDate() { - Calendar cal = Calendar.getInstance(); - cal.set(Calendar.YEAR, 2004); - cal.set(Calendar.MONTH, Calendar.JANUARY); - cal.set(Calendar.DAY_OF_MONTH, 1); - cal.set(Calendar.HOUR, 0); - cal.set(Calendar.MINUTE, 0); - cal.set(Calendar.SECOND, 0); - - return cal.getTime(); - } - - private static Date getNeverExpire() { - Calendar cal = Calendar.getInstance(); - cal.set(Calendar.YEAR, 2999); - cal.set(Calendar.MONTH, 11); - cal.set(Calendar.DAY_OF_MONTH, 28); - cal.set(Calendar.HOUR, 23); - cal.set(Calendar.MINUTE, 59); - cal.set(Calendar.SECOND, 59); - - return cal.getTime(); - } - - public PwDate(byte[] buf, int offset) { - cDate = new byte[DATE_SIZE]; - System.arraycopy(buf, offset, cDate, 0, DATE_SIZE); - cDateBuilt = true; - } - - public PwDate(PwDate source) { - if (source.jDate != null) { - this.jDate = new Date(source.jDate.getTime()); - } - this.jDateBuilt = source.jDateBuilt; - - if (source.cDate != null) { - int dateLength = source.cDate.length; - this.cDate = new byte[dateLength]; - System.arraycopy(source.cDate, 0, this.cDate, 0, dateLength); - } - this.cDateBuilt = source.cDateBuilt; - } - - public PwDate(Date date) { - jDate = new Date(date.getTime()); - jDateBuilt = true; - } - - public PwDate(long millis) { - jDate = new Date(millis); - jDateBuilt = true; - } - - public PwDate() { - jDate = new Date(); - jDateBuilt = true; - } - - protected PwDate(Parcel in) { - jDate = (Date) in.readSerializable(); - jDateBuilt = in.readByte() != 0; - cDateBuilt = false; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeSerializable(getDate()); - dest.writeByte((byte) (jDateBuilt ? 1 : 0)); - } - - public static final Creator CREATOR = new Creator() { - @Override - public PwDate createFromParcel(Parcel in) { - return new PwDate(in); - } - - @Override - public PwDate[] newArray(int size) { - return new PwDate[size]; - } - }; - - public Date getDate() { - if ( ! jDateBuilt ) { - jDate = readTime(cDate, 0, App.getCalendar()); - jDateBuilt = true; - } - - return jDate; - } - - public byte[] getCDate() { - if ( ! cDateBuilt ) { - cDate = writeTime(jDate, App.getCalendar()); - cDateBuilt = true; - } - - return cDate; - } - - /** - * Unpack date from 5 byte format. The five bytes at 'offset' are unpacked - * to a java.util.Date instance. - */ - public static Date readTime(byte[] buf, int offset, Calendar time) { - int dw1 = Types.readUByte(buf, offset); - int dw2 = Types.readUByte(buf, offset + 1); - int dw3 = Types.readUByte(buf, offset + 2); - int dw4 = Types.readUByte(buf, offset + 3); - int dw5 = Types.readUByte(buf, offset + 4); - - // Unpack 5 byte structure to date and time - int year = (dw1 << 6) | (dw2 >> 2); - int month = ((dw2 & 0x00000003) << 2) | (dw3 >> 6); - - int day = (dw3 >> 1) & 0x0000001F; - int hour = ((dw3 & 0x00000001) << 4) | (dw4 >> 4); - int minute = ((dw4 & 0x0000000F) << 2) | (dw5 >> 6); - int second = dw5 & 0x0000003F; - - if (time == null) { - time = Calendar.getInstance(); - } - // File format is a 1 based month, java Calendar uses a zero based month - // File format is a 1 based day, java Calendar uses a 1 based day - time.set(year, month - 1, day, hour, minute, second); - - return time.getTime(); - - } - - public static byte[] writeTime(Date date) { - return writeTime(date, null); - } - - public static byte[] writeTime(Date date, Calendar cal) { - if (date == null) { - return null; - } - - byte[] buf = new byte[5]; - if (cal == null) { - cal = Calendar.getInstance(); - } - cal.setTime(date); - - int year = cal.get(Calendar.YEAR); - // File format is a 1 based month, java Calendar uses a zero based month - int month = cal.get(Calendar.MONTH) + 1; - // File format is a 0 based day, java Calendar uses a 1 based day - int day = cal.get(Calendar.DAY_OF_MONTH) - 1; - int hour = cal.get(Calendar.HOUR_OF_DAY); - int minute = cal.get(Calendar.MINUTE); - int second = cal.get(Calendar.SECOND); - - buf[0] = Types.writeUByte(((year >> 6) & 0x0000003F)); - buf[1] = Types.writeUByte(((year & 0x0000003F) << 2) - | ((month >> 2) & 0x00000003)); - buf[2] = (byte) (((month & 0x00000003) << 6) - | ((day & 0x0000001F) << 1) | ((hour >> 4) & 0x00000001)); - buf[3] = (byte) (((hour & 0x0000000F) << 4) | ((minute >> 2) & 0x0000000F)); - buf[4] = (byte) (((minute & 0x00000003) << 6) | (second & 0x0000003F)); - - return buf; - } - - @Override - public boolean equals(Object o) { - if ( this == o ) { - return true; - } - if ( o == null ) { - return false; - } - if ( getClass() != o.getClass() ) { - return false; - } - - PwDate date = (PwDate) o; - if ( cDateBuilt && date.cDateBuilt ) { - return Arrays.equals(cDate, date.cDate); - } else if ( jDateBuilt && date.jDateBuilt ) { - return IsSameDate(jDate, date.jDate); - } else if ( cDateBuilt && date.jDateBuilt ) { - return Arrays.equals(date.getCDate(), cDate); - } else { - return IsSameDate(date.getDate(), jDate); - } - } - - private static boolean IsSameDate(Date d1, Date d2) { - Calendar cal1 = Calendar.getInstance(); - cal1.setTime(d1); - cal1.set(Calendar.MILLISECOND, 0); - - Calendar cal2 = Calendar.getInstance(); - cal2.setTime(d2); - cal2.set(Calendar.MILLISECOND, 0); - - return (cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)) && - (cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH)) && - (cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH)) && - (cal1.get(Calendar.HOUR) == cal2.get(Calendar.HOUR)) && - (cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE)) && - (cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND)); - - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt new file mode 100644 index 000000000..1c2ec3c47 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt @@ -0,0 +1,284 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.database.element + +import android.os.Parcel +import android.os.Parcelable + +import com.kunzisoft.keepass.utils.Types + +import java.util.Arrays +import java.util.Calendar +import java.util.Date + +/** + * Converting from the C Date format to the Java data format is + * expensive when done for every record at once. + */ +class PwDate : Parcelable { + + private var jDate: Date? = null + private var jDateBuilt = false + @Transient + private var cDate: ByteArray? = null + @Transient + private var cDateBuilt = false + + val date: Date? + get() { + if (!jDateBuilt) { + jDate = readTime(cDate, 0, calendar) + jDateBuilt = true + } + + return jDate + } + + val byteArrayDate: ByteArray? + get() { + if (!cDateBuilt) { + cDate = writeTime(jDate, calendar) + cDateBuilt = true + } + + return cDate + } + + constructor(buf: ByteArray, offset: Int) { + cDate = ByteArray(DATE_SIZE) + System.arraycopy(buf, offset, cDate!!, 0, DATE_SIZE) + cDateBuilt = true + } + + constructor(source: PwDate) { + if (source.jDate != null) { + this.jDate = Date(source.jDate!!.time) + } + this.jDateBuilt = source.jDateBuilt + + if (source.cDate != null) { + val dateLength = source.cDate!!.size + this.cDate = ByteArray(dateLength) + System.arraycopy(source.cDate!!, 0, this.cDate!!, 0, dateLength) + } + this.cDateBuilt = source.cDateBuilt + } + + constructor(date: Date) { + jDate = Date(date.time) + jDateBuilt = true + } + + constructor(millis: Long) { + jDate = Date(millis) + jDateBuilt = true + } + + constructor() { + jDate = Date() + jDateBuilt = true + } + + protected constructor(parcel: Parcel) { + jDate = parcel.readSerializable() as Date + jDateBuilt = parcel.readByte().toInt() != 0 + cDateBuilt = false + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeSerializable(date) + dest.writeByte((if (jDateBuilt) 1 else 0).toByte()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null) { + return false + } + if (javaClass != other.javaClass) { + return false + } + + val date = other as PwDate? + return if (cDateBuilt && date!!.cDateBuilt) { + Arrays.equals(cDate, date.cDate) + } else if (jDateBuilt && date!!.jDateBuilt) { + isSameDate(jDate, date.jDate) + } else if (cDateBuilt && date!!.jDateBuilt) { + Arrays.equals(date.byteArrayDate, cDate) + } else { + isSameDate(date!!.date, jDate) + } + } + + override fun hashCode(): Int { + var result = jDate?.hashCode() ?: 0 + result = 31 * result + jDateBuilt.hashCode() + result = 31 * result + (cDate?.contentHashCode() ?: 0) + result = 31 * result + cDateBuilt.hashCode() + return result + } + + companion object { + + private const val DATE_SIZE = 5 + + private var mCalendar: Calendar? = null + + val NEVER_EXPIRE = neverExpire + val DEFAULT_DATE = defaultDate + + val PW_NEVER_EXPIRE = PwDate(NEVER_EXPIRE) + val DEFAULT_PWDATE = PwDate(DEFAULT_DATE) + + private val calendar: Calendar? + get() { + if (mCalendar == null) { + mCalendar = Calendar.getInstance() + } + return mCalendar + } + + private val defaultDate: Date + get() { + val cal = Calendar.getInstance() + cal.set(Calendar.YEAR, 2004) + cal.set(Calendar.MONTH, Calendar.JANUARY) + cal.set(Calendar.DAY_OF_MONTH, 1) + cal.set(Calendar.HOUR, 0) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + + return cal.time + } + + private val neverExpire: Date + get() { + val cal = Calendar.getInstance() + cal.set(Calendar.YEAR, 2999) + cal.set(Calendar.MONTH, 11) + cal.set(Calendar.DAY_OF_MONTH, 28) + cal.set(Calendar.HOUR, 23) + cal.set(Calendar.MINUTE, 59) + cal.set(Calendar.SECOND, 59) + + return cal.time + } + + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): PwDate { + return PwDate(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + /** + * Unpack date from 5 byte format. The five bytes at 'offset' are unpacked + * to a java.util.Date instance. + */ + fun readTime(buf: ByteArray?, offset: Int, calendar: Calendar?): Date { + var time = calendar + val dw1 = Types.readUByte(buf!!, offset) + val dw2 = Types.readUByte(buf, offset + 1) + val dw3 = Types.readUByte(buf, offset + 2) + val dw4 = Types.readUByte(buf, offset + 3) + val dw5 = Types.readUByte(buf, offset + 4) + + // Unpack 5 byte structure to date and time + val year = dw1 shl 6 or (dw2 shr 2) + val month = dw2 and 0x00000003 shl 2 or (dw3 shr 6) + + val day = dw3 shr 1 and 0x0000001F + val hour = dw3 and 0x00000001 shl 4 or (dw4 shr 4) + val minute = dw4 and 0x0000000F shl 2 or (dw5 shr 6) + val second = dw5 and 0x0000003F + + if (time == null) { + time = Calendar.getInstance() + } + // File format is a 1 based month, java Calendar uses a zero based month + // File format is a 1 based day, java Calendar uses a 1 based day + time!!.set(year, month - 1, day, hour, minute, second) + + return time.time + + } + + @JvmOverloads + fun writeTime(date: Date?, calendar: Calendar? = null): ByteArray? { + var cal = calendar + if (date == null) { + return null + } + + val buf = ByteArray(5) + if (cal == null) { + cal = Calendar.getInstance() + } + cal!!.time = date + + val year = cal.get(Calendar.YEAR) + // File format is a 1 based month, java Calendar uses a zero based month + val month = cal.get(Calendar.MONTH) + 1 + // File format is a 0 based day, java Calendar uses a 1 based day + val day = cal.get(Calendar.DAY_OF_MONTH) - 1 + val hour = cal.get(Calendar.HOUR_OF_DAY) + val minute = cal.get(Calendar.MINUTE) + val second = cal.get(Calendar.SECOND) + + buf[0] = Types.writeUByte(year shr 6 and 0x0000003F) + buf[1] = Types.writeUByte(year and 0x0000003F shl 2 or (month shr 2 and 0x00000003)) + buf[2] = (month and 0x00000003 shl 6 + or (day and 0x0000001F shl 1) or (hour shr 4 and 0x00000001)).toByte() + buf[3] = (hour and 0x0000000F shl 4 or (minute shr 2 and 0x0000000F)).toByte() + buf[4] = (minute and 0x00000003 shl 6 or (second and 0x0000003F)).toByte() + + return buf + } + + private fun isSameDate(d1: Date?, d2: Date?): Boolean { + val cal1 = Calendar.getInstance() + cal1.time = d1 + cal1.set(Calendar.MILLISECOND, 0) + + val cal2 = Calendar.getInstance() + cal2.time = d2 + cal2.set(Calendar.MILLISECOND, 0) + + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && + cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH) && + cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH) && + cal1.get(Calendar.HOUR) == cal2.get(Calendar.HOUR) && + cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE) && + cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND) + + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt index 663cff5b8..7348b00a9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt @@ -283,7 +283,7 @@ class PwEntryV4 : PwEntry, NodeV4Interface { for (i in history.indices) { val entry = history[i] val lastMod = entry.lastModificationTime.date - if (min == null || lastMod.before(min)) { + if (min == null || lastMod == null || lastMod.before(min)) { index = i min = lastMod } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt index 9a019bab2..aa0b11a42 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt @@ -88,7 +88,8 @@ abstract class PwNode, Entry : final override var isExpires: Boolean // If expireDate is before NEVER_EXPIRE date less 1 month (to be sure) - get() = expiryTime.date.before(LocalDate.fromDateFields(PwDate.NEVER_EXPIRE).minusMonths(1).toDate()) + get() = expiryTime.date + ?.before(LocalDate.fromDateFields(PwDate.NEVER_EXPIRE).minusMonths(1).toDate()) ?: true set(value) { if (!value) { expiryTime = PwDate.PW_NEVER_EXPIRE diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwEntryOutputV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwEntryOutputV3.kt index a24c7c1e6..578048ac8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwEntryOutputV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwEntryOutputV3.kt @@ -89,16 +89,16 @@ class PwEntryOutputV3 length += addlLen.toLong() // Create date - writeDate(CREATE_FIELD_TYPE, mPE.creationTime.cDate) + writeDate(CREATE_FIELD_TYPE, mPE.creationTime.byteArrayDate) // Modification date - writeDate(MOD_FIELD_TYPE, mPE.lastModificationTime.cDate) + writeDate(MOD_FIELD_TYPE, mPE.lastModificationTime.byteArrayDate) // Access date - writeDate(ACCESS_FIELD_TYPE, mPE.lastAccessTime.cDate) + writeDate(ACCESS_FIELD_TYPE, mPE.lastAccessTime.byteArrayDate) // Expiration date - writeDate(EXPIRE_FIELD_TYPE, mPE.expiryTime.cDate) + writeDate(EXPIRE_FIELD_TYPE, mPE.expiryTime.byteArrayDate) // Binary desc mOS.write(BINARY_DESC_FIELD_TYPE) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwGroupOutputV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwGroupOutputV3.kt index 247b9c5ae..bbd936539 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwGroupOutputV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwGroupOutputV3.kt @@ -49,22 +49,22 @@ class PwGroupOutputV3 // Create date mOS.write(CREATE_FIELD_TYPE) mOS.write(DATE_FIELD_SIZE) - mOS.write(mPG.creationTime.cDate) + mOS.write(mPG.creationTime.byteArrayDate) // Modification date mOS.write(MOD_FIELD_TYPE) mOS.write(DATE_FIELD_SIZE) - mOS.write(mPG.lastModificationTime.cDate) + mOS.write(mPG.lastModificationTime.byteArrayDate) // Access date mOS.write(ACCESS_FIELD_TYPE) mOS.write(DATE_FIELD_SIZE) - mOS.write(mPG.lastAccessTime.cDate) + mOS.write(mPG.lastAccessTime.byteArrayDate) // Expiration date mOS.write(EXPIRE_FIELD_TYPE) mOS.write(DATE_FIELD_SIZE) - mOS.write(mPG.expiryTime.cDate) + mOS.write(mPG.expiryTime.byteArrayDate) // Image ID mOS.write(IMAGEID_FIELD_TYPE) diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java index 1c7d4c2ea..84e2a9c44 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java @@ -218,7 +218,7 @@ public class FileSelectActivity extends StylishActivity implements if (dbUri != null) scheme = dbUri.getScheme(); - if (!EmptyUtils.isNullOrEmpty(scheme) && scheme.equalsIgnoreCase("file")) { + if (!EmptyUtils.INSTANCE.isNullOrEmpty(scheme) && scheme.equalsIgnoreCase("file")) { String path = dbUri.getPath(); File db = new File(path); diff --git a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java index dd66947db..54e222f87 100644 --- a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java @@ -131,7 +131,7 @@ public class PasswordActivity extends StylishActivity } private static void verifyFileNameUriFromLaunch(String fileName) throws FileNotFoundException { - if (EmptyUtils.isNullOrEmpty(fileName)) { + if (EmptyUtils.INSTANCE.isNullOrEmpty(fileName)) { throw new FileNotFoundException(); } @@ -139,7 +139,7 @@ public class PasswordActivity extends StylishActivity assert uri != null; String scheme = uri.getScheme(); - if (!EmptyUtils.isNullOrEmpty(scheme) && scheme.equalsIgnoreCase("file")) { + if (!EmptyUtils.INSTANCE.isNullOrEmpty(scheme) && scheme.equalsIgnoreCase("file")) { File dbFile = new File(uri.getPath()); if (!dbFile.exists()) { throw new FileNotFoundException(); @@ -471,7 +471,7 @@ public class PasswordActivity extends StylishActivity // Retrieve settings for default database String defaultFilename = prefs.getString(KEY_DEFAULT_FILENAME, ""); if (mDbUri!=null - && !EmptyUtils.isNullOrEmpty(mDbUri.getPath()) + && !EmptyUtils.INSTANCE.isNullOrEmpty(mDbUri.getPath()) && UriUtil.equalsDefaultfile(mDbUri, defaultFilename)) { checkboxDefaultDatabaseView.setChecked(true); } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/EmptyUtils.java b/app/src/main/java/com/kunzisoft/keepass/utils/EmptyUtils.kt similarity index 57% rename from app/src/main/java/com/kunzisoft/keepass/utils/EmptyUtils.java rename to app/src/main/java/com/kunzisoft/keepass/utils/EmptyUtils.kt index 3ea26f3cd..e946af741 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/EmptyUtils.java +++ b/app/src/main/java/com/kunzisoft/keepass/utils/EmptyUtils.kt @@ -17,26 +17,26 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.utils; +package com.kunzisoft.keepass.utils -import android.net.Uri; +import android.net.Uri -import com.kunzisoft.keepass.database.element.PwDate; +import com.kunzisoft.keepass.database.element.PwDate -public class EmptyUtils { - public static boolean isNullOrEmpty(String str) { - return (str == null) || (str.length() == 0); +object EmptyUtils { + fun isNullOrEmpty(str: String?): Boolean { + return str == null || str.isEmpty() } - public static boolean isNullOrEmpty(byte[] buf) { - return (buf == null) || (buf.length == 0); + fun isNullOrEmpty(buf: ByteArray?): Boolean { + return buf == null || buf.isEmpty() } - public static boolean isNullOrEmpty(PwDate date) { - return (date == null) || date.equals(PwDate.DEFAULT_PWDATE); + fun isNullOrEmpty(date: PwDate?): Boolean { + return date == null || date == PwDate.DEFAULT_PWDATE } - public static boolean isNullOrEmpty(Uri uri) { - return (uri==null) || (uri.toString().length() == 0); + fun isNullOrEmpty(uri: Uri?): Boolean { + return uri == null || uri.toString().isEmpty() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.java b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.java index 91b4838a8..f59857b20 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.java +++ b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.java @@ -26,28 +26,25 @@ import android.net.Uri; import com.kunzisoft.keepass.compat.StorageAF; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; /** * Created by bpellin on 3/5/16. */ public class UriUtil { public static Uri parseDefaultFile(String text) { - if (EmptyUtils.isNullOrEmpty(text)) { + if (EmptyUtils.INSTANCE.isNullOrEmpty(text)) { return null; } Uri uri = Uri.parse(text); - if (EmptyUtils.isNullOrEmpty(uri.getScheme())) { + if (EmptyUtils.INSTANCE.isNullOrEmpty(uri.getScheme())) { uri = uri.buildUpon().scheme("file").authority("").build(); } return uri; } public static Uri parseDefaultFile(Uri uri) { - if (EmptyUtils.isNullOrEmpty(uri.getScheme())) { + if (EmptyUtils.INSTANCE.isNullOrEmpty(uri.getScheme())) { uri = uri.buildUpon().scheme("file").authority("").build(); } @@ -72,7 +69,7 @@ public class UriUtil { if (StorageAF.useStorageFramework(ctx) || hasWritableContentUri(uri)) { return uri; } String scheme = uri.getScheme(); - if (EmptyUtils.isNullOrEmpty(scheme)) { return uri; } + if (EmptyUtils.INSTANCE.isNullOrEmpty(scheme)) { return uri; } String filepath = null; @@ -94,7 +91,7 @@ public class UriUtil { } // Try using the URI path as a straight file - if (EmptyUtils.isNullOrEmpty(filepath)) { + if (EmptyUtils.INSTANCE.isNullOrEmpty(filepath)) { filepath = uri.getEncodedPath(); if (!isValidFilePath(filepath)) { filepath = null; @@ -107,7 +104,7 @@ public class UriUtil { } // Update the file to a file URI - if (!EmptyUtils.isNullOrEmpty(filepath)) { + if (!EmptyUtils.INSTANCE.isNullOrEmpty(filepath)) { Uri.Builder b = new Uri.Builder(); uri = b.scheme("file").authority("").path(filepath).build(); } @@ -116,7 +113,7 @@ public class UriUtil { } private static boolean isValidFilePath(String filepath) { - if (EmptyUtils.isNullOrEmpty(filepath)) { return false; } + if (EmptyUtils.INSTANCE.isNullOrEmpty(filepath)) { return false; } File file = new File(filepath); return file.exists() && file.canRead(); @@ -130,7 +127,7 @@ public class UriUtil { private static boolean hasWritableContentUri(Uri uri) { String scheme = uri.getScheme(); - if (EmptyUtils.isNullOrEmpty(scheme)) { return false; } + if (EmptyUtils.INSTANCE.isNullOrEmpty(scheme)) { return false; } if (!scheme.equalsIgnoreCase("content")) { return false; } From 15794f09c38b14abcd49fa550e95e509915a6630 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 4 Jul 2019 19:04:42 +0200 Subject: [PATCH 03/24] Move database history in singleton --- app/src/main/AndroidManifest.xml | 2 +- .../keepass/activities/EntryActivity.java | 6 +- .../keepass/activities/EntryEditActivity.java | 4 +- .../keepass/activities/GroupActivity.java | 24 +- .../activities/lock/LockingActivity.kt | 6 +- .../keepass/adapters/NodeAdapter.java | 4 +- .../java/com/kunzisoft/keepass/app/App.java | 64 ---- .../FileSelectViewHolder.java => app/App.kt} | 35 ++- .../autofill/AutoFillLauncherActivity.kt | 6 +- .../database/action/CreateDatabaseRunnable.kt | 7 +- .../database/action/LoadDatabaseRunnable.kt | 27 +- .../dialogs/GroupEditDialogFragment.java | 2 +- .../DeleteFileHistoryAsyncTask.java | 14 +- ...r.java => FileDatabaseHistoryAdapter.java} | 44 +-- .../FileDatabaseHistoryViewHolder.kt | 34 +++ ...SelectBean.java => FileDatabaseModel.java} | 4 +- ...y.java => FileDatabaseSelectActivity.java} | 91 +++--- .../FileInformationDialogFragment.java | 18 +- .../fileselect/OpenFileHistoryAsyncTask.java | 8 +- .../keepass/fileselect/RecentFileHistory.java | 273 ------------------ .../FileDatabaseHelper.java} | 81 +----- .../database/FileDatabaseHistory.kt | 259 +++++++++++++++++ .../database/FileDatabaseHistoryHelper.java | 58 ++++ .../keepass/icons/IconPackChooser.java | 2 +- .../magikeyboard/KeyboardLauncherActivity.kt | 6 +- .../keepass/password/PasswordActivity.java | 89 +----- .../keepass/password/UriIntentInitTask.kt | 89 ++++++ .../settings/MainPreferenceFragment.java | 2 +- .../settings/NestedSettingsFragment.java | 9 +- ...abaseSavePreferenceDialogFragmentCompat.kt | 2 +- .../keepass/timeout/TimeoutHelper.kt | 4 +- .../keepass/utils/SingletonHolder.kt | 17 ++ gradle.properties | 2 +- 33 files changed, 647 insertions(+), 646 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/app/App.java rename app/src/main/java/com/kunzisoft/keepass/{fileselect/FileSelectViewHolder.java => app/App.kt} (50%) rename app/src/main/java/com/kunzisoft/keepass/fileselect/{FileSelectAdapter.java => FileDatabaseHistoryAdapter.java} (74%) create mode 100644 app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryViewHolder.kt rename app/src/main/java/com/kunzisoft/keepass/fileselect/{FileSelectBean.java => FileDatabaseModel.java} (95%) rename app/src/main/java/com/kunzisoft/keepass/fileselect/{FileSelectActivity.java => FileDatabaseSelectActivity.java} (89%) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/fileselect/RecentFileHistory.java rename app/src/main/java/com/kunzisoft/keepass/fileselect/{FileDbHelper.java => database/FileDatabaseHelper.java} (72%) create mode 100644 app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistory.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistoryHelper.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/password/UriIntentInitTask.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/utils/SingletonHolder.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 69bdaf58e..399f808f7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ android:value="" /> { }); // Database - this.database = App.getDB(); + this.database = App.Companion.getCurrentDatabase(); // Retrieve the color to tint the icon int[] attrTextColorPrimary = {android.R.attr.textColorPrimary}; @@ -340,7 +340,7 @@ public class NodeAdapter extends RecyclerView.Adapter { MenuItem menuItem = contextMenu.findItem(R.id.menu_open); menuItem.setOnMenuItemClickListener(mOnMyActionClickListener); - Database database = App.getDB(); + Database database = App.Companion.getCurrentDatabase(); // Edition if (readOnly || node.equals(database.getRecycleBin())) { diff --git a/app/src/main/java/com/kunzisoft/keepass/app/App.java b/app/src/main/java/com/kunzisoft/keepass/app/App.java deleted file mode 100644 index 93a4aba37..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/app/App.java +++ /dev/null @@ -1,64 +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.app; - -import android.support.multidex.MultiDexApplication; - -import com.kunzisoft.keepass.compat.PRNGFixes; -import com.kunzisoft.keepass.database.element.Database; -import com.kunzisoft.keepass.fileselect.RecentFileHistory; -import com.kunzisoft.keepass.stylish.Stylish; - -public class App extends MultiDexApplication { - private static Database db = null; - private static RecentFileHistory fileHistory = null; - - public static Database getDB() { - if ( db == null ) { - db = new Database(); - } - return db; - } - - public static RecentFileHistory getFileHistory() { - return fileHistory; - } - - public static void setDB(Database d) { - db = d; - } - - @Override - public void onCreate() { - super.onCreate(); - - Stylish.init(this); - fileHistory = new RecentFileHistory(this); - PRNGFixes.apply(); - } - - @Override - public void onTerminate() { - if ( db != null ) { - db.closeAndClear(getApplicationContext()); - } - super.onTerminate(); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectViewHolder.java b/app/src/main/java/com/kunzisoft/keepass/app/App.kt similarity index 50% rename from app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectViewHolder.java rename to app/src/main/java/com/kunzisoft/keepass/app/App.kt index efe29e1b0..0709203a7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectViewHolder.java +++ b/app/src/main/java/com/kunzisoft/keepass/app/App.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 Jeremy Jamet / Kunzisoft. + * Copyright 2019 Jeremy Jamet / Kunzisoft. * * This file is part of KeePass DX. * @@ -17,25 +17,28 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.fileselect; +package com.kunzisoft.keepass.app -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; +import android.support.multidex.MultiDexApplication +import com.kunzisoft.keepass.compat.PRNGFixes +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.stylish.Stylish -import com.kunzisoft.keepass.R; +class App : MultiDexApplication() { -class FileSelectViewHolder extends RecyclerView.ViewHolder { + companion object { + var currentDatabase: Database = Database() + } - View fileContainer; - TextView fileName; - ImageView fileInformation; + override fun onCreate() { + super.onCreate() - FileSelectViewHolder(View itemView) { - super(itemView); - fileContainer = itemView.findViewById(R.id.file_container); - fileName = (TextView) itemView.findViewById(R.id.file_filename); - fileInformation = (ImageView) itemView.findViewById(R.id.file_information); + Stylish.init(this) + PRNGFixes.apply() + } + + override fun onTerminate() { + currentDatabase.closeAndClear(applicationContext) + super.onTerminate() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutoFillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutoFillLauncherActivity.kt index 22919c23e..b107d413c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutoFillLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutoFillLauncherActivity.kt @@ -30,7 +30,7 @@ import android.support.annotation.RequiresApi import android.support.v7.app.AppCompatActivity import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.app.App -import com.kunzisoft.keepass.fileselect.FileSelectActivity +import com.kunzisoft.keepass.fileselect.FileDatabaseSelectActivity import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -41,10 +41,10 @@ class AutoFillLauncherActivity : AppCompatActivity() { // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) val assistStructure = AutofillHelper.retrieveAssistStructure(intent) if (assistStructure != null) { - if (App.getDB().loaded && TimeoutHelper.checkTime(this)) + if (App.currentDatabase.loaded && TimeoutHelper.checkTime(this)) GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this)) else { - FileSelectActivity.launchForAutofillResult(this, assistStructure) + FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure) } } else { setResult(Activity.RESULT_CANCELED) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt index 3ab478d89..6f06eb0c5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt @@ -27,14 +27,11 @@ class CreateDatabaseRunnable(private val mFilename: String, val onDatabaseCreate: (database: Database) -> ActionRunnable) : ActionRunnable() { - var database: Database? = null - override fun run() { try { // Create new database record - database = Database(mFilename) - App.setDB(database) - database?.apply { + Database(mFilename).apply { + App.currentDatabase = this // Set Database state loaded = true // Commit changes diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt index 71c068923..eee176f18 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt @@ -25,15 +25,16 @@ import android.preference.PreferenceManager import android.support.annotation.StringRes import android.util.Log import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.app.App import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.exception.* +import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import java.io.FileNotFoundException import java.io.IOException +import java.lang.ref.WeakReference -class LoadDatabaseRunnable(private val mContext: Context, +class LoadDatabaseRunnable(private val mWeakContext: WeakReference, private val mDatabase: Database, private val mUri: Uri, private val mPass: String?, @@ -42,14 +43,22 @@ class LoadDatabaseRunnable(private val mContext: Context, nestedAction: ActionRunnable) : ActionRunnable(nestedAction, executeNestedActionIfResultFalse = true) { - private val mRememberKeyFile: Boolean = PreferenceManager.getDefaultSharedPreferences(mContext) - .getBoolean(mContext.getString(R.string.keyfile_key), mContext.resources.getBoolean(R.bool.keyfile_default)) + private val mRememberKeyFile: Boolean + get() { + return mWeakContext.get()?.let { + PreferenceManager.getDefaultSharedPreferences(it) + .getBoolean(it.getString(R.string.keyfile_key), + it.resources.getBoolean(R.bool.keyfile_default)) + } ?: true + } override fun run() { try { - mDatabase.loadData(mContext, mUri, mPass, mKey, progressTaskUpdater) - saveFileData(mUri, mKey) - finishRun(true) + mWeakContext.get()?.let { + mDatabase.loadData(it, mUri, mPass, mKey, progressTaskUpdater) + saveFileData(mUri, mKey) + finishRun(true) + } ?: finishRun(false, "Context null") } catch (e: ArcFourException) { catchError(e, R.string.error_arc4) return @@ -98,7 +107,7 @@ class LoadDatabaseRunnable(private val mContext: Context, } private fun catchError(e: Throwable, @StringRes messageId: Int, addThrowableMessage: Boolean = false) { - var errorMessage = mContext.getString(messageId) + var errorMessage = mWeakContext.get()?.getString(messageId) Log.e(TAG, errorMessage, e) if (addThrowableMessage) errorMessage = errorMessage + " " + e.localizedMessage @@ -110,7 +119,7 @@ class LoadDatabaseRunnable(private val mContext: Context, if (!mRememberKeyFile) { keyFileUri = null } - App.getFileHistory().createFile(uri, keyFileUri) + FileDatabaseHistory.getInstance(mWeakContext).addDatabaseUri(uri, keyFileUri) } override fun onFinishRun(isSuccess: Boolean, message: String?) {} diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/GroupEditDialogFragment.java b/app/src/main/java/com/kunzisoft/keepass/dialogs/GroupEditDialogFragment.java index 354e36f56..fa576a400 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/GroupEditDialogFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/dialogs/GroupEditDialogFragment.java @@ -118,7 +118,7 @@ public class GroupEditDialogFragment extends DialogFragment iconColor = ta.getColor(0, Color.WHITE); // Init elements - database = App.getDB(); + database = App.Companion.getCurrentDatabase(); editGroupDialogAction = EditGroupDialogAction.NONE; nameGroup = ""; iconGroup = database.getIconFactory().getFolderIcon(); diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.java index f8ceed45a..3d4ffad56 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.java @@ -21,20 +21,22 @@ package com.kunzisoft.keepass.fileselect; import android.os.AsyncTask; -class DeleteFileHistoryAsyncTask extends AsyncTask { +import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory; + +class DeleteFileHistoryAsyncTask extends AsyncTask { private AfterDeleteFileHistoryListener afterDeleteFileHistoryListener; - private RecentFileHistory fileHistory; - private FileSelectAdapter adapter; + private FileDatabaseHistory fileHistory; + private FileDatabaseHistoryAdapter adapter; - DeleteFileHistoryAsyncTask(AfterDeleteFileHistoryListener afterDeleteFileHistoryListener, RecentFileHistory fileHistory, FileSelectAdapter adapter) { + DeleteFileHistoryAsyncTask(AfterDeleteFileHistoryListener afterDeleteFileHistoryListener, FileDatabaseHistory fileHistory, FileDatabaseHistoryAdapter adapter) { this.afterDeleteFileHistoryListener = afterDeleteFileHistoryListener; this.fileHistory = fileHistory; this.adapter = adapter; } - protected Void doInBackground(FileSelectBean... args) { - fileHistory.deleteFile(args[0].getFileUri()); + protected Void doInBackground(FileDatabaseModel... args) { + fileHistory.deleteDatabaseUri(args[0].getFileUri()); return null; } diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectAdapter.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.java similarity index 74% rename from app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectAdapter.java rename to app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.java index 468586d48..4c69b2cbc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectAdapter.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.java @@ -38,7 +38,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil; import java.util.List; -public class FileSelectAdapter extends RecyclerView.Adapter { +public class FileDatabaseHistoryAdapter extends RecyclerView.Adapter { private static final int MENU_CLEAR = 1; @@ -54,7 +54,7 @@ public class FileSelectAdapter extends RecyclerView.Adapter listFiles) { + FileDatabaseHistoryAdapter(Context context, List listFiles) { this.inflater = LayoutInflater.from(context); this.context = context; this.listFiles = listFiles; @@ -69,28 +69,28 @@ public class FileSelectAdapter extends RecyclerView.Adapter. + * + */ +package com.kunzisoft.keepass.fileselect + +import android.support.v7.widget.RecyclerView +import android.view.View +import android.widget.ImageView +import android.widget.TextView + +import com.kunzisoft.keepass.R + +internal class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + var fileContainer: View = itemView.findViewById(R.id.file_container) + var fileName: TextView = itemView.findViewById(R.id.file_filename) + var fileInformation: ImageView = itemView.findViewById(R.id.file_information) +} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectBean.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseModel.java similarity index 95% rename from app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectBean.java rename to app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseModel.java index c62d9c978..07de0acd8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectBean.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseModel.java @@ -27,7 +27,7 @@ import java.io.File; import java.io.Serializable; import java.util.Date; -public class FileSelectBean implements Serializable { +public class FileDatabaseModel implements Serializable { private static final String EXTERNAL_STORAGE_AUTHORITY = "com.android.externalstorage.documents"; @@ -36,7 +36,7 @@ public class FileSelectBean implements Serializable { private Date lastModification; private long size; - public FileSelectBean(Context context, String pathFile) { + public FileDatabaseModel(Context context, String pathFile) { fileName = ""; lastModification = new Date(); size = 0; diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java similarity index 89% rename from app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java rename to app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java index 84e2a9c44..80f0b323a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileSelectActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java @@ -50,10 +50,10 @@ import com.getkeepsafe.taptargetview.TapTargetView; import com.kunzisoft.keepass.R; import com.kunzisoft.keepass.activities.EntrySelectionHelper; import com.kunzisoft.keepass.activities.GroupActivity; -import com.kunzisoft.keepass.app.App; import com.kunzisoft.keepass.autofill.AutofillHelper; import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable; import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable; +import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory; import com.kunzisoft.keepass.tasks.ActionRunnable; import com.kunzisoft.keepass.database.action.ProgressDialogRunnable; import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment; @@ -73,6 +73,7 @@ import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.lang.ref.WeakReference; import java.net.URLDecoder; import permissions.dispatcher.NeedsPermission; @@ -83,24 +84,24 @@ import permissions.dispatcher.PermissionRequest; import permissions.dispatcher.RuntimePermissions; @RuntimePermissions -public class FileSelectActivity extends StylishActivity implements +public class FileDatabaseSelectActivity extends StylishActivity implements CreateFileDialogFragment.DefinePathDialogListener, AssignMasterKeyDialogFragment.AssignPasswordDialogListener, - FileSelectAdapter.FileItemOpenListener, - FileSelectAdapter.FileSelectClearListener, - FileSelectAdapter.FileInformationShowListener { + FileDatabaseHistoryAdapter.FileItemOpenListener, + FileDatabaseHistoryAdapter.FileSelectClearListener, + FileDatabaseHistoryAdapter.FileInformationShowListener { - private static final String TAG = "FileSelectActivity"; + private static final String TAG = "FileDatabaseSelectActivity"; private static final String EXTRA_STAY = "EXTRA_STAY"; - private FileSelectAdapter mAdapter; + private FileDatabaseHistoryAdapter mAdapter; private View fileListContainer; private View createButtonView; private View browseButtonView; private View openButtonView; - private RecentFileHistory fileHistory; + private FileDatabaseHistory fileDatabaseHistory; private View fileSelectExpandableButton; private ExpandableLayout fileSelectExpandable; @@ -125,7 +126,7 @@ public class FileSelectActivity extends StylishActivity implements */ public static void launchForKeyboardSelection(Activity activity) { - KeyboardHelper.INSTANCE.startActivityForKeyboardSelection(activity, new Intent(activity, FileSelectActivity.class)); + KeyboardHelper.INSTANCE.startActivityForKeyboardSelection(activity, new Intent(activity, FileDatabaseSelectActivity.class)); } /* @@ -137,7 +138,7 @@ public class FileSelectActivity extends StylishActivity implements @RequiresApi(api = Build.VERSION_CODES.O) public static void launchForAutofillResult(Activity activity, @NonNull AssistStructure assistStructure) { AutofillHelper.INSTANCE.startActivityForAutofillResult(activity, - new Intent(activity, FileSelectActivity.class), + new Intent(activity, FileDatabaseSelectActivity.class), assistStructure); } @@ -145,7 +146,7 @@ public class FileSelectActivity extends StylishActivity implements protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - fileHistory = App.getFileHistory(); + fileDatabaseHistory = FileDatabaseHistory.Companion.getInstance(new WeakReference<>(getApplicationContext())); setContentView(R.layout.file_selection); fileListContainer = findViewById(R.id.container_file_list); @@ -189,8 +190,8 @@ public class FileSelectActivity extends StylishActivity implements // Create button createButtonView = findViewById(R.id.create_database); createButtonView .setOnClickListener(v -> - FileSelectActivityPermissionsDispatcher - .openCreateFileDialogFragmentWithPermissionCheck(FileSelectActivity.this) + FileDatabaseSelectActivityPermissionsDispatcher + .openCreateFileDialogFragmentWithPermissionCheck(FileDatabaseSelectActivity.this) ); keyFileHelper = new KeyFileHelper(this); @@ -199,7 +200,7 @@ public class FileSelectActivity extends StylishActivity implements () -> Uri.parse("file://" + openFileNameView.getText().toString()))); // Construct adapter with listeners - mAdapter = new FileSelectAdapter(FileSelectActivity.this, fileHistory.getDbList()); + mAdapter = new FileDatabaseHistoryAdapter(FileDatabaseSelectActivity.this, fileDatabaseHistory.getDatabaseUriList()); mAdapter.setOnItemClickListener(this); mAdapter.setFileSelectClearListener(this); mAdapter.setFileInformationShowListener(this); @@ -238,7 +239,7 @@ public class FileSelectActivity extends StylishActivity implements private void fileNoFoundAction(FileNotFoundException e) { String error = getString(R.string.file_not_found_content); - Toast.makeText(FileSelectActivity.this, + Toast.makeText(FileDatabaseSelectActivity.this, error, Toast.LENGTH_LONG).show(); Log.e(TAG, error, e); } @@ -247,7 +248,7 @@ public class FileSelectActivity extends StylishActivity implements EntrySelectionHelper.INSTANCE.doEntrySelectionAction(getIntent(), () -> { try { - PasswordActivity.launch(FileSelectActivity.this, + PasswordActivity.launch(FileDatabaseSelectActivity.this, fileName, keyFile); } catch (FileNotFoundException e) { fileNoFoundAction(e); @@ -256,7 +257,7 @@ public class FileSelectActivity extends StylishActivity implements }, () -> { try { - PasswordActivity.launchForKeyboardResult(FileSelectActivity.this, + PasswordActivity.launchForKeyboardResult(FileDatabaseSelectActivity.this, fileName, keyFile); finish(); } catch (FileNotFoundException e) { @@ -267,7 +268,7 @@ public class FileSelectActivity extends StylishActivity implements assistStructure -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { - PasswordActivity.launchForAutofillResult(FileSelectActivity.this, + PasswordActivity.launchForAutofillResult(FileDatabaseSelectActivity.this, fileName, keyFile, assistStructure); } catch (FileNotFoundException e) { @@ -320,7 +321,7 @@ public class FileSelectActivity extends StylishActivity implements private void checkAndPerformedEducation() { // If no recent files - if ( !fileHistory.hasRecentFiles() ) { + if ( !fileDatabaseHistory.hasRecentFiles() ) { // Try to open the creation base education if (!PreferencesUtil.isEducationCreateDatabasePerformed(this) ) { @@ -336,8 +337,8 @@ public class FileSelectActivity extends StylishActivity implements @Override public void onTargetClick(TapTargetView view) { super.onTargetClick(view); - FileSelectActivityPermissionsDispatcher - .openCreateFileDialogFragmentWithPermissionCheck(FileSelectActivity.this); + FileDatabaseSelectActivityPermissionsDispatcher + .openCreateFileDialogFragmentWithPermissionCheck(FileDatabaseSelectActivity.this); } @Override @@ -348,7 +349,7 @@ public class FileSelectActivity extends StylishActivity implements checkAndPerformedEducationForSelection(); } }); - PreferencesUtil.saveEducationPreference(FileSelectActivity.this, + PreferencesUtil.saveEducationPreference(FileDatabaseSelectActivity.this, R.string.education_create_db_key); } } @@ -366,7 +367,7 @@ public class FileSelectActivity extends StylishActivity implements if (!PreferencesUtil.isEducationSelectDatabasePerformed(this) && browseButtonView != null) { - TapTargetView.showFor(FileSelectActivity.this, + TapTargetView.showFor(FileDatabaseSelectActivity.this, TapTarget.forView(browseButtonView, getString(R.string.education_select_database_title), getString(R.string.education_select_database_summary)) @@ -386,13 +387,13 @@ public class FileSelectActivity extends StylishActivity implements super.onOuterCircleClick(view); view.dismiss(false); - if (!PreferencesUtil.isEducationOpenLinkDatabasePerformed(FileSelectActivity.this)) { + if (!PreferencesUtil.isEducationOpenLinkDatabasePerformed(FileDatabaseSelectActivity.this)) { - TapTargetView.showFor(FileSelectActivity.this, + TapTargetView.showFor(FileDatabaseSelectActivity.this, TapTarget.forView(fileSelectExpandableButton, getString(R.string.education_open_link_database_title), getString(R.string.education_open_link_database_summary)) - .icon(ContextCompat.getDrawable(FileSelectActivity.this, R.drawable.ic_link_white_24dp)) + .icon(ContextCompat.getDrawable(FileDatabaseSelectActivity.this, R.drawable.ic_link_white_24dp)) .textColorInt(Color.WHITE) .tintTarget(true) .cancelable(true), @@ -409,12 +410,12 @@ public class FileSelectActivity extends StylishActivity implements view.dismiss(false); } }); - PreferencesUtil.saveEducationPreference(FileSelectActivity.this, + PreferencesUtil.saveEducationPreference(FileDatabaseSelectActivity.this, R.string.education_open_link_db_key); } } }); - PreferencesUtil.saveEducationPreference(FileSelectActivity.this, + PreferencesUtil.saveEducationPreference(FileDatabaseSelectActivity.this, R.string.education_select_db_key); } } @@ -431,7 +432,7 @@ public class FileSelectActivity extends StylishActivity implements public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); // NOTE: delegate the permission handling to generated method - FileSelectActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults); + FileDatabaseSelectActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults); } @NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) @@ -457,7 +458,7 @@ public class FileSelectActivity extends StylishActivity implements // Make sure file name exists if (pathString.length() == 0) { Log.e(TAG, getString(R.string.error_filename_required)); - Toast.makeText(FileSelectActivity.this, + Toast.makeText(FileDatabaseSelectActivity.this, R.string.error_filename_required, Toast.LENGTH_LONG).show(); return false; @@ -468,7 +469,7 @@ public class FileSelectActivity extends StylishActivity implements try { if (file.exists()) { Log.e(TAG, getString(R.string.error_database_exists) + " " + file); - Toast.makeText(FileSelectActivity.this, + Toast.makeText(FileDatabaseSelectActivity.this, R.string.error_database_exists, Toast.LENGTH_LONG).show(); return false; @@ -477,7 +478,7 @@ public class FileSelectActivity extends StylishActivity implements if ( parent == null || (parent.exists() && ! parent.isDirectory()) ) { Log.e(TAG, getString(R.string.error_invalid_path) + " " + file); - Toast.makeText(FileSelectActivity.this, + Toast.makeText(FileDatabaseSelectActivity.this, R.string.error_invalid_path, Toast.LENGTH_LONG).show(); return false; @@ -487,7 +488,7 @@ public class FileSelectActivity extends StylishActivity implements // Create parent directory if ( ! parent.mkdirs() ) { Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + parent); - Toast.makeText(FileSelectActivity.this, + Toast.makeText(FileDatabaseSelectActivity.this, R.string.error_could_not_create_parent, Toast.LENGTH_LONG).show(); return false; @@ -499,7 +500,7 @@ public class FileSelectActivity extends StylishActivity implements Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + e.getLocalizedMessage()); e.printStackTrace(); Toast.makeText( - FileSelectActivity.this, + FileDatabaseSelectActivity.this, getText(R.string.error_file_not_create) + " " + e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); @@ -538,7 +539,7 @@ public class FileSelectActivity extends StylishActivity implements progressTaskUpdater -> new CreateDatabaseRunnable(databaseFilename, database -> { // TODO store database created - return new AssignPasswordInDatabaseRunnable(FileSelectActivity.this, + return new AssignPasswordInDatabaseRunnable(FileDatabaseSelectActivity.this, database, masterPasswordChecked, masterPassword, @@ -578,10 +579,10 @@ public class FileSelectActivity extends StylishActivity implements runOnUiThread(() -> { if (isSuccess) { // Add database to recent files - fileHistory.createFile(fileURI); + fileDatabaseHistory.addDatabaseUri(fileURI); mAdapter.notifyDataSetChanged(); updateFileListVisibility(); - GroupActivity.launch(FileSelectActivity.this); + GroupActivity.launch(FileDatabaseSelectActivity.this); } else { Log.e(TAG, "Unable to open the database"); } @@ -601,25 +602,25 @@ public class FileSelectActivity extends StylishActivity implements new OpenFileHistoryAsyncTask((fileName, keyFile) -> { launchPasswordActivity(fileName, keyFile); updateFileListVisibility(); - }, fileHistory).execute(itemPosition); + }, fileDatabaseHistory).execute(itemPosition); } @Override - public void onClickFileInformation(FileSelectBean fileSelectBean) { - if (fileSelectBean != null) { + public void onClickFileInformation(FileDatabaseModel fileDatabaseModel) { + if (fileDatabaseModel != null) { FileInformationDialogFragment fileInformationDialogFragment = - FileInformationDialogFragment.newInstance(fileSelectBean); + FileInformationDialogFragment.newInstance(fileDatabaseModel); fileInformationDialogFragment.show(getSupportFragmentManager(), "fileInformation"); } } @Override - public boolean onFileSelectClearListener(final FileSelectBean fileSelectBean) { + public boolean onFileSelectClearListener(final FileDatabaseModel fileDatabaseModel) { new DeleteFileHistoryAsyncTask(() -> { - fileHistory.deleteFile(fileSelectBean.getFileUri()); + fileDatabaseHistory.deleteDatabaseUri(fileDatabaseModel.getFileUri()); mAdapter.notifyDataSetChanged(); updateFileListVisibility(); - }, fileHistory, mAdapter).execute(fileSelectBean); + }, fileDatabaseHistory, mAdapter).execute(fileDatabaseModel); return true; } @@ -634,7 +635,7 @@ public class FileSelectActivity extends StylishActivity implements keyFileHelper.onActivityResultCallback(requestCode, resultCode, data, uri -> { if (uri != null) { - if (PreferencesUtil.autoOpenSelectedFile(FileSelectActivity.this)) { + if (PreferencesUtil.autoOpenSelectedFile(FileDatabaseSelectActivity.this)) { launchPasswordActivityWithPath(uri.toString()); } else { fileSelectExpandable.expand(false); diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileInformationDialogFragment.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileInformationDialogFragment.java index 726fa1b79..0e793117d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileInformationDialogFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileInformationDialogFragment.java @@ -40,11 +40,11 @@ public class FileInformationDialogFragment extends DialogFragment { private View fileSizeContainerView; private View fileModificationContainerView; - public static FileInformationDialogFragment newInstance(FileSelectBean fileSelectBean) { + public static FileInformationDialogFragment newInstance(FileDatabaseModel fileDatabaseModel) { FileInformationDialogFragment fileInformationDialogFragment = new FileInformationDialogFragment(); Bundle args = new Bundle(); - args.putSerializable(FILE_SELECT_BEEN_ARG, fileSelectBean); + args.putSerializable(FILE_SELECT_BEEN_ARG, fileDatabaseModel); fileInformationDialogFragment.setArguments(args); return fileInformationDialogFragment; } @@ -63,19 +63,19 @@ public class FileInformationDialogFragment extends DialogFragment { TextView fileModificationView = root.findViewById(R.id.file_modification); if (getArguments() != null && getArguments().containsKey(FILE_SELECT_BEEN_ARG)) { - FileSelectBean fileSelectBean = (FileSelectBean) getArguments().getSerializable(FILE_SELECT_BEEN_ARG); - if(fileSelectBean != null) { + FileDatabaseModel fileDatabaseModel = (FileDatabaseModel) getArguments().getSerializable(FILE_SELECT_BEEN_ARG); + if(fileDatabaseModel != null) { - filePathView.setText(Uri.decode(fileSelectBean.getFileUri().toString())); - fileNameView.setText(fileSelectBean.getFileName()); + filePathView.setText(Uri.decode(fileDatabaseModel.getFileUri().toString())); + fileNameView.setText(fileDatabaseModel.getFileName()); - if(fileSelectBean.notFound()) { + if(fileDatabaseModel.notFound()) { hideFileInfo(); } else { showFileInfo(); - fileSizeView.setText(String.valueOf(fileSelectBean.getSize())); + fileSizeView.setText(String.valueOf(fileDatabaseModel.getSize())); fileModificationView.setText(DateFormat.getDateTimeInstance() - .format(fileSelectBean.getLastModification())); + .format(fileDatabaseModel.getLastModification())); } } else hideFileInfo(); diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.java index 872ef8179..39169eed7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.java @@ -21,14 +21,16 @@ package com.kunzisoft.keepass.fileselect; import android.os.AsyncTask; +import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory; + class OpenFileHistoryAsyncTask extends AsyncTask { private AfterOpenFileHistoryListener afterOpenFileHistoryListener; - private RecentFileHistory fileHistory; + private FileDatabaseHistory fileHistory; private String fileName; private String keyFile; - OpenFileHistoryAsyncTask(AfterOpenFileHistoryListener afterOpenFileHistoryListener, RecentFileHistory fileHistory) { + OpenFileHistoryAsyncTask(AfterOpenFileHistoryListener afterOpenFileHistoryListener, FileDatabaseHistory fileHistory) { this.afterOpenFileHistoryListener = afterOpenFileHistoryListener; this.fileHistory = fileHistory; } @@ -36,7 +38,7 @@ class OpenFileHistoryAsyncTask extends AsyncTask { protected Void doInBackground(Integer... args) { int position = args[0]; fileName = fileHistory.getDatabaseAt(position); - keyFile = fileHistory.getKeyfileAt(position); + keyFile = fileHistory.getKeyFileAt(position); return null; } diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/RecentFileHistory.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/RecentFileHistory.java deleted file mode 100644 index 381107d80..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/RecentFileHistory.java +++ /dev/null @@ -1,273 +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.fileselect; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.database.Cursor; -import android.net.Uri; -import android.preference.PreferenceManager; - -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.utils.UriUtil; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -public class RecentFileHistory { - - private static String DB_KEY = "recent_databases"; - private static String KEYFILE_KEY = "recent_keyfiles"; - - private List databases = new ArrayList(); - private List keyfiles = new ArrayList(); - private Context ctx; - private SharedPreferences prefs; - private OnSharedPreferenceChangeListener listner; - private boolean enabled; - private boolean init = false; - - public RecentFileHistory(Context c) { - ctx = c.getApplicationContext(); - - prefs = PreferenceManager.getDefaultSharedPreferences(c); - enabled = prefs.getBoolean(ctx.getString(R.string.recentfile_key), ctx.getResources().getBoolean(R.bool.recentfile_default)); - listner = new OnSharedPreferenceChangeListener() { - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, - String key) { - if (key.equals(ctx.getString(R.string.recentfile_key))) { - enabled = sharedPreferences.getBoolean(ctx.getString(R.string.recentfile_key), ctx.getResources().getBoolean(R.bool.recentfile_default)); - } - } - }; - prefs.registerOnSharedPreferenceChangeListener(listner); - } - - private synchronized void init() { - if (!init) { - if (!upgradeFromSQL()) { - loadPrefs(); - } - - init = true; - } - } - - private boolean upgradeFromSQL() { - - try { - // Check for a database to upgrade from - if (!sqlDatabaseExists()) { - return false; - } - - databases.clear(); - keyfiles.clear(); - - FileDbHelper helper = new FileDbHelper(ctx); - helper.open(); - Cursor cursor = helper.fetchAllFiles(); - - int dbIndex = cursor.getColumnIndex(FileDbHelper.KEY_FILE_FILENAME); - int keyIndex = cursor.getColumnIndex(FileDbHelper.KEY_FILE_KEYFILE); - - if(cursor.moveToFirst()) { - while (cursor.moveToNext()) { - String filename = cursor.getString(dbIndex); - String keyfile = cursor.getString(keyIndex); - - databases.add(filename); - keyfiles.add(keyfile); - } - } - - savePrefs(); - - cursor.close(); - helper.close(); - - } catch (Exception e) { - // If upgrading fails, we'll just give up on it. - } - - try { - FileDbHelper.deleteDatabase(ctx); - } catch (Exception e) { - // If we fail to delete it, just move on - } - - return true; - } - - private boolean sqlDatabaseExists() { - File db = ctx.getDatabasePath(FileDbHelper.DATABASE_NAME); - return db.exists(); - } - - public void createFile(Uri databaseUri) { - createFile(databaseUri, null); - } - - public void createFile(Uri databaseUri, Uri keyFileUri) { - if (!enabled || databaseUri == null) return; - - init(); - - // Remove any existing instance of the same filename - deleteFile(databaseUri, false); - - databases.add(0, databaseUri.toString()); - - String key = (keyFileUri == null) ? "" : keyFileUri.toString(); - keyfiles.add(0, key); - - trimLists(); - savePrefs(); - } - - public boolean hasRecentFiles() { - if (!enabled) return false; - - init(); - - return databases.size() > 0; - } - - public String getDatabaseAt(int i) { - init(); - return databases.get(i); - } - - public String getKeyfileAt(int i) { - init(); - return keyfiles.get(i); - } - - private void loadPrefs() { - loadList(databases, DB_KEY); - loadList(keyfiles, KEYFILE_KEY); - } - - private void savePrefs() { - saveList(DB_KEY, databases); - saveList(KEYFILE_KEY, keyfiles); - } - - private void loadList(List list, String keyprefix) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); - int size = prefs.getInt(keyprefix, 0); - - list.clear(); - for (int i = 0; i < size; i++) { - list.add(prefs.getString(keyprefix + "_" + i, "")); - } - } - - private void saveList(String keyprefix, List list) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); - SharedPreferences.Editor edit = prefs.edit(); - int size = list.size(); - edit.putInt(keyprefix, size); - - for (int i = 0; i < size; i++) { - edit.putString(keyprefix + "_" + i, list.get(i)); - } - edit.apply(); - } - - public void deleteFile(Uri uri) { - deleteFile(uri, true); - } - - public void deleteFile(Uri uri, boolean save) { - init(); - - String uriName = uri.toString(); - String fileName = uri.getPath(); - - for (int i = 0; i < databases.size(); i++) { - String entry = databases.get(i); - if (uriName.equals(entry) || fileName.equals(entry)) { - databases.remove(i); - keyfiles.remove(i); - break; - } - } - - if (save) { - savePrefs(); - } - } - - public List getDbList() { - init(); - - return databases; - } - - public Uri getFileByName(Uri database) { - if (!enabled) return null; - - init(); - - int size = databases.size(); - for (int i = 0; i < size; i++) { - if (UriUtil.equalsDefaultfile(database,databases.get(i))) { - return UriUtil.parseDefaultFile(keyfiles.get(i)); - } - } - - return null; - } - - public void deleteAll() { - init(); - - databases.clear(); - keyfiles.clear(); - - savePrefs(); - } - - public void deleteAllKeys() { - init(); - - keyfiles.clear(); - - int size = databases.size(); - for (int i = 0; i < size; i++) { - keyfiles.add(""); - } - - savePrefs(); - } - - private void trimLists() { - int size = databases.size(); - for (int i = FileDbHelper.MAX_FILES; i < size; i++) { - databases.remove(i); - keyfiles.remove(i); - } - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDbHelper.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHelper.java similarity index 72% rename from app/src/main/java/com/kunzisoft/keepass/fileselect/FileDbHelper.java rename to app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHelper.java index 2738173b6..cad5ae967 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDbHelper.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHelper.java @@ -17,30 +17,27 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.fileselect; +package com.kunzisoft.keepass.fileselect.database; import android.content.ContentValues; import android.content.Context; -import android.content.SharedPreferences; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.util.Log; import java.io.File; import java.io.FileFilter; -public class FileDbHelper { +public class FileDatabaseHelper { - private static final String TAG = FileDbHelper.class.getName(); + private static final String TAG = FileDatabaseHelper.class.getName(); - public static final String LAST_FILENAME = "lastFile"; - public static final String LAST_KEYFILE = "lastKey"; + static final String LAST_FILENAME = "lastFile"; + static final String LAST_KEYFILE = "lastKey"; public static final String DATABASE_NAME = "keepassdroid"; // TODO Change db name - private static final String FILE_TABLE = "files"; - private static final int DATABASE_VERSION = 1; + static final String FILE_TABLE = "files"; + static final int DATABASE_VERSION = 1; public static final int MAX_FILES = 5; @@ -49,74 +46,20 @@ public class FileDbHelper { public static final String KEY_FILE_KEYFILE = "keyFile"; public static final String KEY_FILE_UPDATED = "updated"; - private static final String DATABASE_CREATE = + static final String DATABASE_CREATE = "create table " + FILE_TABLE + " ( " + KEY_FILE_ID + " integer primary key autoincrement, " + KEY_FILE_FILENAME + " text not null, " + KEY_FILE_KEYFILE + " text, " + KEY_FILE_UPDATED + " integer not null);"; private final Context mCtx; - private DatabaseHelper mDbHelper; private SQLiteDatabase mDb; - private static class DatabaseHelper extends SQLiteOpenHelper { - private final Context mCtx; - - DatabaseHelper(Context ctx) { - super(ctx, DATABASE_NAME, null, DATABASE_VERSION); - mCtx = ctx; - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL(DATABASE_CREATE); - - // Migrate preference to database if it is set. - SharedPreferences settings = mCtx.getSharedPreferences("PasswordActivity", Context.MODE_PRIVATE); - String lastFile = settings.getString(LAST_FILENAME, ""); - String lastKey = settings.getString(LAST_KEYFILE,""); - - if ( lastFile.length() > 0 ) { - ContentValues vals = new ContentValues(); - vals.put(KEY_FILE_FILENAME, lastFile); - vals.put(KEY_FILE_UPDATED, System.currentTimeMillis()); - - if ( lastKey.length() > 0 ) { - vals.put(KEY_FILE_KEYFILE, lastKey); - } - - db.insert(FILE_TABLE, null, vals); - - // Clear old preferences - deletePrefs(settings); - - } - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // Only one database version so far - } - - private void deletePrefs(SharedPreferences prefs) { - // We won't worry too much if this fails - try { - SharedPreferences.Editor editor = prefs.edit(); - editor.remove(LAST_FILENAME); - editor.remove(LAST_KEYFILE); - editor.apply(); - } catch (Exception e) { - Log.e(TAG, "Unable to delete database preference", e); - } - } - } - - public FileDbHelper(Context ctx) { + public FileDatabaseHelper(Context ctx) { mCtx = ctx; } - public FileDbHelper open() throws SQLException { - mDbHelper = new DatabaseHelper(mCtx); - mDb = mDbHelper.getWritableDatabase(); + public FileDatabaseHelper open() throws SQLException { + mDb = new FileDatabaseHistoryHelper(mCtx).getWritableDatabase(); return this; } @@ -136,7 +79,6 @@ public class FileDbHelper { cursor = mDb.query(true, FILE_TABLE, new String[] {KEY_FILE_ID}, KEY_FILE_FILENAME + "=?", new String[] {fileName}, null, null, null, null); } catch (Exception e ) { - assert(true); return -1; } @@ -167,7 +109,6 @@ public class FileDbHelper { deleteAllBut(MAX_FILES); } catch (Exception e) { e.printStackTrace(); - assert(true); } cursor.close(); diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistory.kt b/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistory.kt new file mode 100644 index 000000000..b6126e53d --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistory.kt @@ -0,0 +1,259 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.fileselect.database + +import android.content.Context +import android.content.SharedPreferences +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.net.Uri +import android.preference.PreferenceManager + +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.utils.SingletonHolder +import com.kunzisoft.keepass.utils.UriUtil +import java.lang.ref.WeakReference + +import java.util.ArrayList + +class FileDatabaseHistory private constructor(private val context: WeakReference) { + + private val mDatabasesUriList = ArrayList() + private val mKeyFilesUriList = ArrayList() + + private val mPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.get()) + var isEnabled: Boolean = false + + val databaseUriList: List + get() { + init() + return mDatabasesUriList + } + + private val onSharedPreferenceChangeListener = OnSharedPreferenceChangeListener { sharedPreferences, key -> + if (key == context.get()?.getString(R.string.recentfile_key)) { + isEnabled = sharedPreferences.getBoolean( + context.get()?.getString(R.string.recentfile_key), + context.get()?.resources?.getBoolean(R.bool.recentfile_default) ?: isEnabled) + } + } + + init { + mPreferences.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) + context.get()?.resources?.let { + isEnabled = mPreferences.getBoolean( + it.getString(R.string.recentfile_key), + it.getBoolean(R.bool.recentfile_default)) + } + mPreferences.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) + } + + private var init = false + @Synchronized + private fun init() { + if (!init) { + if (!upgradeFromSQL()) { + loadPrefs() + } + + init = true + } + } + + private fun upgradeFromSQL(): Boolean { + + try { + // Check for a database to upgrade from + if (context.get()?.getDatabasePath(FileDatabaseHelper.DATABASE_NAME)?.exists() != true) { + return false + } + + mDatabasesUriList.clear() + mKeyFilesUriList.clear() + + val helper = FileDatabaseHelper(context.get()) + helper.open() + val cursor = helper.fetchAllFiles() + + val dbIndex = cursor.getColumnIndex(FileDatabaseHelper.KEY_FILE_FILENAME) + val keyIndex = cursor.getColumnIndex(FileDatabaseHelper.KEY_FILE_KEYFILE) + + if (cursor.moveToFirst()) { + while (cursor.moveToNext()) { + mDatabasesUriList.add(cursor.getString(dbIndex)) + mKeyFilesUriList.add(cursor.getString(keyIndex)) + } + } + + savePrefs() + + cursor.close() + helper.close() + + } catch (e: Exception) { + // If upgrading fails, we'll just give up on it. + } + + try { + FileDatabaseHelper.deleteDatabase(context.get()) + } catch (e: Exception) { + // If we fail to delete it, just move on + } + + return true + } + + @JvmOverloads + fun addDatabaseUri(databaseUri: Uri?, keyFileUri: Uri? = null) { + if (!isEnabled || databaseUri == null) return + + init() + + // Remove any existing instance of the same filename + deleteDatabaseUri(databaseUri, false) + + mDatabasesUriList.add(0, databaseUri.toString()) + + val key = keyFileUri?.toString() ?: "" + mKeyFilesUriList.add(0, key) + + trimLists() + savePrefs() + } + + fun hasRecentFiles(): Boolean { + if (!isEnabled) return false + + init() + + return mDatabasesUriList.size > 0 + } + + fun getDatabaseAt(i: Int): String { + init() + return mDatabasesUriList[i] + } + + fun getKeyFileAt(i: Int): String { + init() + return mKeyFilesUriList[i] + } + + private fun loadPrefs() { + loadList(DB_KEY, mDatabasesUriList) + loadList(KEY_FILE_KEY, mKeyFilesUriList) + } + + private fun savePrefs() { + saveList(DB_KEY, mDatabasesUriList) + saveList(KEY_FILE_KEY, mKeyFilesUriList) + } + + private fun loadList(keyPrefix: String, list: MutableList) { + val size = mPreferences.getInt(keyPrefix, 0) + + list.clear() + for (i in 0 until size) { + list.add(mPreferences.getString(keyPrefix + "_" + i, "")) + } + } + + private fun saveList(keyPrefix: String, list: List) { + val edit = mPreferences.edit() + val size = list.size + edit.putInt(keyPrefix, size) + + for (i in 0 until size) { + edit.putString(keyPrefix + "_" + i, list[i]) + } + edit.apply() + } + + @JvmOverloads + fun deleteDatabaseUri(uri: Uri, save: Boolean = true) { + init() + + val uriName = uri.toString() + val fileName = uri.path + + for (i in mDatabasesUriList.indices) { + val entry = mDatabasesUriList[i] + if (uriName == entry || fileName == entry) { + mDatabasesUriList.removeAt(i) + mKeyFilesUriList.removeAt(i) + break + } + } + + if (save) { + savePrefs() + } + } + + fun getKeyFileUriByDatabaseUri(database: Uri): Uri? { + if (!isEnabled) return null + + init() + + val size = mDatabasesUriList.size + for (i in 0 until size) { + if (UriUtil.equalsDefaultfile(database, mDatabasesUriList[i])) { + return UriUtil.parseDefaultFile(mKeyFilesUriList[i]) + } + } + + return null + } + + fun deleteAll() { + init() + + mDatabasesUriList.clear() + mKeyFilesUriList.clear() + + savePrefs() + } + + fun deleteAllKeys() { + init() + + mKeyFilesUriList.clear() + + val size = mDatabasesUriList.size + for (i in 0 until size) { + mKeyFilesUriList.add("") + } + + savePrefs() + } + + private fun trimLists() { + val size = mDatabasesUriList.size + for (i in FileDatabaseHelper.MAX_FILES until size) { + mDatabasesUriList.removeAt(i) + mKeyFilesUriList.removeAt(i) + } + } + + companion object : SingletonHolder>(::FileDatabaseHistory) { + + private const val DB_KEY = "recent_databases" + private const val KEY_FILE_KEY = "recent_keyfiles" + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistoryHelper.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistoryHelper.java new file mode 100644 index 000000000..cae05e737 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistoryHelper.java @@ -0,0 +1,58 @@ +package com.kunzisoft.keepass.fileselect.database; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +class FileDatabaseHistoryHelper extends SQLiteOpenHelper { + private final SharedPreferences settings; + + FileDatabaseHistoryHelper(Context context) { + super(context, FileDatabaseHelper.DATABASE_NAME, null, FileDatabaseHelper.DATABASE_VERSION); + settings = context.getSharedPreferences("PasswordActivity", Context.MODE_PRIVATE); + } + + @Override + public void onCreate(SQLiteDatabase sqLiteDatabase) { + sqLiteDatabase.execSQL(FileDatabaseHelper.DATABASE_CREATE); + + // Migrate preference to database if it is set. + String lastFile = settings.getString(FileDatabaseHelper.LAST_FILENAME, ""); + String lastKey = settings.getString(FileDatabaseHelper.LAST_KEYFILE,""); + + if ( lastFile.length() > 0 ) { + ContentValues contentValues = new ContentValues(); + contentValues.put(FileDatabaseHelper.KEY_FILE_FILENAME, lastFile); + contentValues.put(FileDatabaseHelper.KEY_FILE_UPDATED, System.currentTimeMillis()); + + if ( lastKey.length() > 0 ) { + contentValues.put(FileDatabaseHelper.KEY_FILE_KEYFILE, lastKey); + } + + sqLiteDatabase.insert(FileDatabaseHelper.FILE_TABLE, null, contentValues); + + // Clear old preferences + deletePrefs(settings); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Only one database version so far + } + + private void deletePrefs(SharedPreferences prefs) { + // We won't worry too much if this fails + try { + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(FileDatabaseHelper.LAST_FILENAME); + editor.remove(FileDatabaseHelper.LAST_KEYFILE); + editor.apply(); + } catch (Exception e) { + Log.e(FileDatabaseHelper.class.getName(), "Unable to delete database preference", e); + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/icons/IconPackChooser.java b/app/src/main/java/com/kunzisoft/keepass/icons/IconPackChooser.java index 32967bea5..72ee9435f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/icons/IconPackChooser.java +++ b/app/src/main/java/com/kunzisoft/keepass/icons/IconPackChooser.java @@ -108,7 +108,7 @@ public class IconPackChooser { public static void setSelectedIconPack(String iconPackIdString) { for(IconPack iconPack : iconPackList) { if (iconPack.getId().equals(iconPackIdString)) { - App.getDB().getDrawFactory().clearCache(); + App.Companion.getCurrentDatabase().getDrawFactory().clearCache(); iconPackSelected = iconPack; break; } diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt index 2d3b87308..875509217 100644 --- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt @@ -4,7 +4,7 @@ import android.os.Bundle import android.support.v7.app.AppCompatActivity import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.app.App -import com.kunzisoft.keepass.fileselect.FileSelectActivity +import com.kunzisoft.keepass.fileselect.FileDatabaseSelectActivity import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -15,11 +15,11 @@ class KeyboardLauncherActivity : AppCompatActivity() { } override fun onCreate(savedInstanceState: Bundle?) { - if (App.getDB().loaded && TimeoutHelper.checkTime(this)) + if (App.currentDatabase.loaded && TimeoutHelper.checkTime(this)) GroupActivity.launchForKeyboardSelection(this, PreferencesUtil.enableReadOnlyDatabase(this)) else { // Pass extra to get entry - FileSelectActivity.launchForKeyboardSelection(this) + FileDatabaseSelectActivity.launchForKeyboardSelection(this) } finish() super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java index 54e222f87..4bcc6c778 100644 --- a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java @@ -28,7 +28,6 @@ import android.content.SharedPreferences; import android.graphics.Color; import android.hardware.fingerprint.FingerprintManager; import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -56,7 +55,6 @@ import com.kunzisoft.keepass.activities.ReadOnlyHelper; import com.kunzisoft.keepass.activities.lock.LockingActivity; import com.kunzisoft.keepass.app.App; import com.kunzisoft.keepass.autofill.AutofillHelper; -import com.kunzisoft.keepass.compat.ClipDataCompat; import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable; import com.kunzisoft.keepass.database.action.ProgressDialogRunnable; import com.kunzisoft.keepass.database.element.Database; @@ -77,6 +75,7 @@ import permissions.dispatcher.*; import java.io.File; import java.io.FileNotFoundException; +import java.lang.ref.WeakReference; @RuntimePermissions public class PasswordActivity extends StylishActivity @@ -282,7 +281,7 @@ public class PasswordActivity extends StylishActivity protected void onResume() { // If the database isn't accessible make sure to clear the password field, if it // was saved in the instance state - if (App.getDB().getLoaded()) { + if (App.Companion.getCurrentDatabase().getLoaded()) { setEmptyViews(); } @@ -316,7 +315,7 @@ public class PasswordActivity extends StylishActivity checkboxPasswordView.setOnCheckedChangeListener(enableButtonOncheckedChangeListener); } - new UriIntentInitTask(this, mRememberKeyfile) + new UriIntentInitTask(new WeakReference<>(this), this, mRememberKeyfile) .execute(getIntent()); } @@ -868,7 +867,7 @@ public class PasswordActivity extends StylishActivity private void loadDatabase(String password, Uri keyfile) { // Clear before we load - Database database = App.getDB(); + Database database = App.Companion.getCurrentDatabase(); database.closeAndClear(getApplicationContext()); // Show the progress dialog and load the database @@ -876,7 +875,7 @@ public class PasswordActivity extends StylishActivity this, R.string.loading_database, progressTaskUpdater -> new LoadDatabaseRunnable( - PasswordActivity.this, + new WeakReference<>(PasswordActivity.this), database, mDbUri, password, @@ -1030,88 +1029,12 @@ public class PasswordActivity extends StylishActivity case LockingActivity.RESULT_EXIT_LOCK: case Activity.RESULT_CANCELED: setEmptyViews(); - App.getDB().closeAndClear(getApplicationContext()); + App.Companion.getCurrentDatabase().closeAndClear(getApplicationContext()); break; } } } - private static class UriIntentInitTask extends AsyncTask { - - static final String KEY_FILENAME = "fileName"; - static final String KEY_KEYFILE = "keyFile"; - private static final String VIEW_INTENT = "android.intent.action.VIEW"; - - private UriIntentInitTaskCallback uriIntentInitTaskCallback; - private boolean isKeyFileNeeded; - private Uri databaseUri; - private Uri keyFileUri; - - UriIntentInitTask(UriIntentInitTaskCallback uriIntentInitTaskCallback, boolean isKeyFileNeeded) { - this.uriIntentInitTaskCallback = uriIntentInitTaskCallback; - this.isKeyFileNeeded = isKeyFileNeeded; - } - - @Override - protected Integer doInBackground(Intent... args) { - Intent intent = args[0]; - String action = intent.getAction(); - if (action != null && action.equals(VIEW_INTENT)) { - Uri incoming = intent.getData(); - databaseUri = incoming; - keyFileUri = ClipDataCompat.getUriFromIntent(intent, KEY_KEYFILE); - - if (incoming == null) { - return R.string.error_can_not_handle_uri; - } else if (incoming.getScheme().equals("file")) { - String fileName = incoming.getPath(); - - if (fileName.length() == 0) { - // No file name - return R.string.file_not_found; - } - - File dbFile = new File(fileName); - if (!dbFile.exists()) { - // File does not exist - return R.string.file_not_found; - } - - if (keyFileUri == null) { - keyFileUri = getKeyFile(databaseUri); - } - } else if (incoming.getScheme().equals("content")) { - if (keyFileUri == null) { - keyFileUri = getKeyFile(databaseUri); - } - } else { - return R.string.error_can_not_handle_uri; - } - - } else { - databaseUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_FILENAME)); - keyFileUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_KEYFILE)); - - if (keyFileUri == null || keyFileUri.toString().length() == 0) { - keyFileUri = getKeyFile(databaseUri); - } - } - return null; - } - - public void onPostExecute(Integer result) { - uriIntentInitTaskCallback.onPostInitTask(databaseUri, keyFileUri, result); - } - - private Uri getKeyFile(Uri dbUri) { - if (isKeyFileNeeded) { - return App.getFileHistory().getFileByName(dbUri); - } else { - return null; - } - } - } - @NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) public void doNothing() {} diff --git a/app/src/main/java/com/kunzisoft/keepass/password/UriIntentInitTask.kt b/app/src/main/java/com/kunzisoft/keepass/password/UriIntentInitTask.kt new file mode 100644 index 000000000..c82b8a500 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/password/UriIntentInitTask.kt @@ -0,0 +1,89 @@ +package com.kunzisoft.keepass.password + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.AsyncTask + +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.compat.ClipDataCompat +import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory +import com.kunzisoft.keepass.utils.UriUtil + +import java.io.File +import java.lang.ref.WeakReference + +internal class UriIntentInitTask(private val weakContext: WeakReference, + private val uriIntentInitTaskCallback: UriIntentInitTaskCallback, + private val isKeyFileNeeded: Boolean) + : AsyncTask() { + + private var databaseUri: Uri? = null + private var keyFileUri: Uri? = null + + override fun doInBackground(vararg args: Intent): Int? { + val intent = args[0] + val action = intent.action + + // If is a view intent + if (action != null && action == VIEW_INTENT) { + val incoming = intent.data + databaseUri = incoming + keyFileUri = ClipDataCompat.getUriFromIntent(intent, KEY_KEYFILE) + + if (incoming == null) { + return R.string.error_can_not_handle_uri + } else if (incoming.scheme == "file") { + val fileName = incoming.path + + if (fileName!!.isEmpty()) { + // No file name + return R.string.file_not_found + } + + val dbFile = File(fileName) + if (!dbFile.exists()) { + // File does not exist + return R.string.file_not_found + } + + if (keyFileUri == null) { + keyFileUri = getKeyFileUri(databaseUri) + } + } else if (incoming.scheme == "content") { + if (keyFileUri == null) { + keyFileUri = getKeyFileUri(databaseUri) + } + } else { + return R.string.error_can_not_handle_uri + } + + } else { + databaseUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_FILENAME)) + keyFileUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_KEYFILE)) + + if (keyFileUri == null || keyFileUri!!.toString().isEmpty()) { + keyFileUri = getKeyFileUri(databaseUri) + } + } + return null + } + + public override fun onPostExecute(result: Int?) { + uriIntentInitTaskCallback.onPostInitTask(databaseUri, keyFileUri, result) + } + + private fun getKeyFileUri(databaseUri: Uri?): Uri? { + return if (isKeyFileNeeded) { + FileDatabaseHistory.getInstance(weakContext).getKeyFileUriByDatabaseUri(databaseUri!!) + } else { + null + } + } + + companion object { + const val KEY_FILENAME = "fileName" + const val KEY_KEYFILE = "keyFile" + private const val VIEW_INTENT = "android.intent.action.VIEW" + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.java b/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.java index 76514ef49..82cc8743b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.java @@ -59,7 +59,7 @@ public class MainPreferenceFragment extends PreferenceFragmentCompat implements preference = findPreference(getString(R.string.db_key)); preference.setOnPreferenceClickListener(this); - Database db = App.getDB(); + Database db = App.Companion.getCurrentDatabase(); if (!(db.getLoaded())) { preference.setEnabled(false); } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java index d7fd7dfd6..e9e4cea73 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java @@ -49,6 +49,7 @@ import com.kunzisoft.keepass.database.element.Database; import com.kunzisoft.keepass.dialogs.ProFeatureDialogFragment; import com.kunzisoft.keepass.dialogs.UnavailableFeatureDialogFragment; import com.kunzisoft.keepass.dialogs.UnderDevelopmentFeatureDialogFragment; +import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory; import com.kunzisoft.keepass.fingerprint.FingerPrintHelper; import com.kunzisoft.keepass.icons.IconPackChooser; import com.kunzisoft.keepass.dialogs.KeyboardExplanationDialogFragment; @@ -61,6 +62,8 @@ import com.kunzisoft.keepass.settings.preferencedialogfragment.ParallelismPrefer import com.kunzisoft.keepass.settings.preferencedialogfragment.RoundsPreferenceDialogFragmentCompat; import com.kunzisoft.keepass.stylish.Stylish; +import java.lang.ref.WeakReference; + public class NestedSettingsFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener { @@ -123,7 +126,7 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat if (getArguments() != null) key = getArguments().getInt(TAG_KEY); - database = App.getDB(); + database = App.Companion.getCurrentDatabase(); databaseReadOnly = ReadOnlyHelper.INSTANCE.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, getArguments()); databaseReadOnly = database.isReadOnly() || databaseReadOnly; @@ -138,7 +141,7 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat keyFile.setOnPreferenceChangeListener((preference, newValue) -> { Boolean value = (Boolean) newValue; if (!value) { - App.getFileHistory().deleteAllKeys(); + FileDatabaseHistory.Companion.getInstance(new WeakReference<>(getContext().getApplicationContext())).deleteAllKeys(); } return true; }); @@ -150,7 +153,7 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat value = true; } if (!value) { - App.getFileHistory().deleteAll(); + FileDatabaseHistory.Companion.getInstance(new WeakReference<>(getContext().getApplicationContext())).deleteAll(); } return true; }); diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt index 234016aca..d97b5b233 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt @@ -34,7 +34,7 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo override fun onBindDialogView(view: View) { super.onBindDialogView(view) - this.database = App.getDB() + this.database = App.currentDatabase } override fun onDialogClosed(positiveResult: Boolean) { diff --git a/app/src/main/java/com/kunzisoft/keepass/timeout/TimeoutHelper.kt b/app/src/main/java/com/kunzisoft/keepass/timeout/TimeoutHelper.kt index 9227aea1b..61df440c6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/timeout/TimeoutHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/timeout/TimeoutHelper.kt @@ -62,7 +62,7 @@ object TimeoutHelper { edit.putLong(context.getString(R.string.timeout_backup_key), time) edit.apply() - if (App.getDB().loaded) { + if (App.currentDatabase.loaded) { val timeout = try { java.lang.Long.parseLong(prefs.getString(context.getString(R.string.app_timeout_key), context.getString(R.string.clipboard_timeout_default))) @@ -91,7 +91,7 @@ object TimeoutHelper { return true // Cancel the lock PendingIntent - if (App.getDB().loaded) { + if (App.currentDatabase.loaded) { val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager Log.d(TAG, "TimeoutHelper cancel") am.cancel(getLockPendingIntent(context)) diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/SingletonHolder.kt b/app/src/main/java/com/kunzisoft/keepass/utils/SingletonHolder.kt new file mode 100644 index 000000000..f0ab79c8c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/utils/SingletonHolder.kt @@ -0,0 +1,17 @@ +package com.kunzisoft.keepass.utils + +open class SingletonHolder(private val constructor: (A) -> T) { + + @Volatile + private var instance: T? = null + + fun getInstance(arg: A): T { + return when { + instance != null -> instance!! + else -> synchronized(this) { + if (instance == null) instance = constructor(arg) + instance!! + } + } + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 5a4e1dae0..b3c7a0330 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -org.gradle.jvmargs=-Xmx4608M +org.gradle.jvmargs=-Xmx2048M From 77c397e0d110b776d7dfa6d81a78d17c60d594ca Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 4 Jul 2019 19:10:33 +0200 Subject: [PATCH 04/24] Fix small bugs --- .../keepass/activities/AboutActivity.java | 88 ------------------- .../keepass/activities/AboutActivity.kt | 82 +++++++++++++++++ .../FileDatabaseSelectActivity.java | 2 +- 3 files changed, 83 insertions(+), 89 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.java deleted file mode 100644 index 2a2d41516..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.activities; - -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Bundle; -import android.support.v7.widget.Toolbar; -import android.util.Log; -import android.view.MenuItem; -import android.widget.TextView; - -import com.kunzisoft.keepass.BuildConfig; -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.stylish.StylishActivity; - -import org.joda.time.DateTime; - -public class AboutActivity extends StylishActivity { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.about); - - Toolbar toolbar = findViewById(R.id.toolbar); - toolbar.setTitle(getString(R.string.menu_about)); - setSupportActionBar(toolbar); - assert getSupportActionBar() != null; - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setDisplayShowHomeEnabled(true); - - String version; - String build; - try { - version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; - build = BuildConfig.BUILD_VERSION; - } catch (NameNotFoundException e) { - Log.w(getClass().getSimpleName(), "Unable to get the app or the build version", e); - version = "Unable to get the app version"; - build = "Unable to get the build version"; - } - version = getString(R.string.version_label, version); - TextView versionTextView = findViewById(R.id.activity_about_version); - versionTextView.setText(version); - - build = getString(R.string.build_label, build); - TextView buildTextView = findViewById(R.id.activity_about_build); - buildTextView.setText(build); - - - TextView disclaimerText = findViewById(R.id.disclaimer); - disclaimerText.setText(getString(R.string.disclaimer_formal, new DateTime().getYear())); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - switch (id) { - case android.R.id.home: - finish(); - break; - } - return super.onOptionsItemSelected(item); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt new file mode 100644 index 000000000..c2018c06c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.activities + +import android.content.pm.PackageManager.NameNotFoundException +import android.os.Bundle +import android.support.v7.widget.Toolbar +import android.util.Log +import android.view.MenuItem +import android.widget.TextView + +import com.kunzisoft.keepass.BuildConfig +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.stylish.StylishActivity + +import org.joda.time.DateTime + +class AboutActivity : StylishActivity() { + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.about) + + val toolbar = findViewById(R.id.toolbar) + toolbar.title = getString(R.string.menu_about) + setSupportActionBar(toolbar) + assert(supportActionBar != null) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + supportActionBar!!.setDisplayShowHomeEnabled(true) + + var version: String + var build: String + try { + version = packageManager.getPackageInfo(packageName, 0).versionName + build = BuildConfig.BUILD_VERSION + } catch (e: NameNotFoundException) { + Log.w(javaClass.simpleName, "Unable to get the app or the build version", e) + version = "Unable to get the app version" + build = "Unable to get the build version" + } + + version = getString(R.string.version_label, version) + val versionTextView = findViewById(R.id.activity_about_version) + versionTextView.text = version + + build = getString(R.string.build_label, build) + val buildTextView = findViewById(R.id.activity_about_build) + buildTextView.text = build + + + val disclaimerText = findViewById(R.id.disclaimer) + disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + when (item.itemId) { + android.R.id.home -> finish() + } + return super.onOptionsItemSelected(item) + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java index 80f0b323a..1ee943508 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java @@ -91,7 +91,7 @@ public class FileDatabaseSelectActivity extends StylishActivity implements FileDatabaseHistoryAdapter.FileSelectClearListener, FileDatabaseHistoryAdapter.FileInformationShowListener { - private static final String TAG = "FileDatabaseSelectActivity"; + private static final String TAG = "FileDbSelectActivity"; private static final String EXTRA_STAY = "EXTRA_STAY"; From e8b341c69f147f9c8eafb8843fb634c02ddf66cb Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 7 Jul 2019 15:23:47 +0200 Subject: [PATCH 05/24] Refactor Education --- .../keepass/activities/EntryActivity.java | 115 +++---- .../keepass/activities/EntryEditActivity.java | 118 ++++---- .../keepass/activities/GroupActivity.java | 199 ++++--------- .../kunzisoft/keepass/education/Education.kt | 281 ++++++++++++++++++ .../education/EntryActivityEducation.kt | 69 +++++ .../education/EntryEditActivityEducation.kt | 66 ++++ .../FileDatabaseSelectActivityEducation.kt | 102 +++++++ .../education/GroupActivityEducation.kt | 112 +++++++ .../education/PasswordActivityEducation.kt | 90 ++++++ .../FileDatabaseSelectActivity.java | 153 +++------- .../keepass/password/PasswordActivity.java | 166 ++++------- .../settings/NestedSettingsFragment.java | 11 +- .../keepass/settings/PreferencesUtil.java | 215 -------------- app/src/main/res/values/donottranslate.xml | 2 + 14 files changed, 968 insertions(+), 731 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/education/Education.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java index 09f2dfcb8..09a27ea44 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java @@ -37,8 +37,7 @@ import android.view.MenuItem; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; -import com.getkeepsafe.taptargetview.TapTarget; -import com.getkeepsafe.taptargetview.TapTargetView; + import com.kunzisoft.keepass.R; import com.kunzisoft.keepass.activities.lock.LockingHideActivity; import com.kunzisoft.keepass.app.App; @@ -46,6 +45,7 @@ import com.kunzisoft.keepass.database.element.Database; import com.kunzisoft.keepass.database.element.EntryVersioned; import com.kunzisoft.keepass.database.element.PwNodeId; import com.kunzisoft.keepass.database.element.security.ProtectedString; +import com.kunzisoft.keepass.education.EntryActivityEducation; import com.kunzisoft.keepass.notifications.NotificationCopyingService; import com.kunzisoft.keepass.notifications.NotificationField; import com.kunzisoft.keepass.settings.PreferencesUtil; @@ -240,79 +240,6 @@ public class EntryActivity extends LockingHideActivity { firstLaunchOfActivity = false; } - /** - * Check and display learning views - * Displays the explanation for copying a field and editing an entry - */ - private void checkAndPerformedEducation(Menu menu) { - if (PreferencesUtil.isEducationScreensEnabled(this)) { - - if (entryContentsView != null && entryContentsView.isUserNamePresent() - && !PreferencesUtil.isEducationCopyUsernamePerformed(this)) { - TapTargetView.showFor(this, - TapTarget.forView(findViewById(R.id.entry_user_name_action_image), - getString(R.string.education_field_copy_title), - getString(R.string.education_field_copy_summary)) - .textColorInt(Color.WHITE) - .tintTarget(false) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(), - getString(R.string.copy_field, getString(R.string.entry_user_name))); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - // Launch autofill settings - startActivity(new Intent(EntryActivity.this, SettingsAutofillActivity.class)); - } - }); - PreferencesUtil.saveEducationPreference(this, - R.string.education_copy_username_key); - - } else if (!PreferencesUtil.isEducationEntryEditPerformed(this)) { - - try { - TapTargetView.showFor(this, - TapTarget.forToolbarMenuItem(toolbar, R.id.menu_edit, - getString(R.string.education_entry_edit_title), - getString(R.string.education_entry_edit_summary)) - .textColorInt(Color.WHITE) - .tintTarget(true) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - MenuItem editItem = menu.findItem(R.id.menu_edit); - onOptionsItemSelected(editItem); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - // Open Keepass doc to create field references - Intent browserIntent = new Intent(Intent.ACTION_VIEW, - Uri.parse(getString(R.string.field_references_url))); - startActivity(browserIntent); - } - }); - PreferencesUtil.saveEducationPreference(this, - R.string.education_entry_edit_key); - } catch (Exception e) { - // If icon not visible - Log.w(TAG, "Can't performed education for entry's edition"); - } - } - } - } - protected void fillData() { Database database = App.Companion.getCurrentDatabase(); database.startManageEntry(mEntry); @@ -460,11 +387,45 @@ public class EntryActivity extends LockingHideActivity { } // Show education views - new Handler().post(() -> checkAndPerformedEducation(menu)); - + new Handler().post(() -> performedNextEducation(new EntryActivityEducation(this), menu)); + return true; } + private void performedNextEducation(EntryActivityEducation entryActivityEducation, + Menu menu) { + if (entryContentsView != null + && entryContentsView.isUserNamePresent() + && entryActivityEducation.checkAndPerformedEntryCopyEducation( + findViewById(R.id.entry_user_name_action_image), + tapTargetView -> { + clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(), + getString(R.string.copy_field, + getString(R.string.entry_user_name))); + return null; + }, + tapTargetView -> { + // Launch autofill settings + startActivity(new Intent(EntryActivity.this, SettingsAutofillActivity.class)); + return null; + }) + ); + else if (toolbar.findViewById(R.id.menu_edit) != null + && entryActivityEducation.checkAndPerformedEntryEditEducation( + toolbar.findViewById(R.id.menu_edit), + tapTargetView -> { + onOptionsItemSelected(menu.findItem(R.id.menu_edit)); + return null; + }, + tapTargetView -> { + // Open Keepass doc to create field references + startActivity(new Intent(Intent.ACTION_VIEW, + Uri.parse(getString(R.string.field_references_url)))); + return null; + }) + ); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch ( item.getItemId() ) { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.java index bffdcffc8..5ee760dcb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.java @@ -24,12 +24,20 @@ import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Color; import android.os.Bundle; +import android.os.Handler; import android.support.v7.widget.Toolbar; import android.util.Log; -import android.view.*; -import android.widget.*; -import com.getkeepsafe.taptargetview.TapTarget; -import com.getkeepsafe.taptargetview.TapTargetView; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.Toast; + import com.kunzisoft.keepass.R; import com.kunzisoft.keepass.activities.lock.LockingHideActivity; import com.kunzisoft.keepass.app.App; @@ -37,16 +45,24 @@ import com.kunzisoft.keepass.database.action.node.ActionNodeValues; import com.kunzisoft.keepass.database.action.node.AddEntryRunnable; import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable; import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable; -import com.kunzisoft.keepass.database.element.*; +import com.kunzisoft.keepass.database.element.Database; +import com.kunzisoft.keepass.database.element.EntryVersioned; +import com.kunzisoft.keepass.database.element.GroupVersioned; +import com.kunzisoft.keepass.database.element.PwDate; +import com.kunzisoft.keepass.database.element.PwIcon; +import com.kunzisoft.keepass.database.element.PwIconStandard; +import com.kunzisoft.keepass.database.element.PwNodeId; import com.kunzisoft.keepass.database.element.security.ProtectedString; import com.kunzisoft.keepass.dialogs.GeneratePasswordDialogFragment; import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment; +import com.kunzisoft.keepass.education.EntryEditActivityEducation; import com.kunzisoft.keepass.settings.PreferencesUtil; import com.kunzisoft.keepass.tasks.ActionRunnable; import com.kunzisoft.keepass.timeout.TimeoutHelper; import com.kunzisoft.keepass.utils.MenuUtil; import com.kunzisoft.keepass.utils.Util; import com.kunzisoft.keepass.view.EntryEditCustomField; + import org.jetbrains.annotations.NotNull; import static com.kunzisoft.keepass.dialogs.IconPickerDialogFragment.KEY_ICON_STANDARD; @@ -90,6 +106,9 @@ public class EntryEditActivity extends LockingHideActivity private View saveView; private int iconColor; + // Education + private EntryEditActivityEducation entryEditActivityEducation; + /** * Launch EntryEditActivity to update an existing entry * @@ -218,8 +237,33 @@ public class EntryEditActivity extends LockingHideActivity } // Verify the education views - checkAndPerformedEducation(); - } + entryEditActivityEducation = new EntryEditActivityEducation(this); + new Handler().post(() -> performedNextEducation(entryEditActivityEducation)); + } + + private void performedNextEducation(EntryEditActivityEducation entryEditActivityEducation) { + if (entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( + generatePasswordView, + tapTargetView -> { + openPasswordGenerator(); + return null; + }, + tapTargetView -> { + performedNextEducation(entryEditActivityEducation); + return null; + } + )); + else if (mEntry.allowExtraFields() + && !mEntry.containsCustomFields() + && entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( + addNewFieldView, + tapTargetView -> { + addNewCustomField(); + return null; + }, + tapTargetView -> null) + ); + } /** * Open the password generator fragment @@ -283,65 +327,7 @@ public class EntryEditActivity extends LockingHideActivity new Thread(task).start(); } - /** - * Check and display learning views - * Displays the explanation for the icon selection, the password generator and for a new field - */ - private void checkAndPerformedEducation() { - if (PreferencesUtil.isEducationScreensEnabled(this)) { - // TODO Show icon - if (!PreferencesUtil.isEducationPasswordGeneratorPerformed(this)) { - TapTargetView.showFor(this, - TapTarget.forView(generatePasswordView, - getString(R.string.education_generate_password_title), - getString(R.string.education_generate_password_summary)) - .textColorInt(Color.WHITE) - .tintTarget(false) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - openPasswordGenerator(); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - } - }); - PreferencesUtil.saveEducationPreference(this, - R.string.education_password_generator_key); - } else if (mEntry.allowExtraFields() - && !mEntry.containsCustomFields() - && !PreferencesUtil.isEducationEntryNewFieldPerformed(this)) { - TapTargetView.showFor(this, - TapTarget.forView(addNewFieldView, - getString(R.string.education_entry_new_field_title), - getString(R.string.education_entry_new_field_summary)) - .textColorInt(Color.WHITE) - .tintTarget(false) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - addNewCustomField(); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - } - }); - PreferencesUtil.saveEducationPreference(this, - R.string.education_entry_new_field_key); - } - } - } /** * Utility class to retrieve a validation or an error with a message @@ -549,7 +535,7 @@ public class EntryEditActivity extends LockingHideActivity entryPasswordView.setText(generatedPassword); entryConfirmationPasswordView.setText(generatedPassword); - checkAndPerformedEducation(); + new Handler().post(() -> performedNextEducation(entryEditActivityEducation)); } @Override diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java index 382afbf94..3b69e8688 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java @@ -51,8 +51,6 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.getkeepsafe.taptargetview.TapTarget; -import com.getkeepsafe.taptargetview.TapTargetView; import com.kunzisoft.keepass.R; import com.kunzisoft.keepass.activities.lock.LockingActivity; import com.kunzisoft.keepass.adapters.NodeAdapter; @@ -71,7 +69,12 @@ import com.kunzisoft.keepass.database.action.node.DeleteGroupRunnable; import com.kunzisoft.keepass.database.action.node.MoveEntryRunnable; import com.kunzisoft.keepass.database.action.node.MoveGroupRunnable; import com.kunzisoft.keepass.database.action.node.UpdateGroupRunnable; -import com.kunzisoft.keepass.database.element.*; +import com.kunzisoft.keepass.database.element.Database; +import com.kunzisoft.keepass.database.element.EntryVersioned; +import com.kunzisoft.keepass.database.element.GroupVersioned; +import com.kunzisoft.keepass.database.element.NodeVersioned; +import com.kunzisoft.keepass.database.element.PwIcon; +import com.kunzisoft.keepass.database.element.PwNodeId; import com.kunzisoft.keepass.database.element.security.ProtectedString; import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment; import com.kunzisoft.keepass.dialogs.GroupEditDialogFragment; @@ -79,6 +82,7 @@ import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment; import com.kunzisoft.keepass.dialogs.PasswordEncodingDialogHelper; import com.kunzisoft.keepass.dialogs.ReadOnlyDialog; import com.kunzisoft.keepass.dialogs.SortDialogFragment; +import com.kunzisoft.keepass.education.GroupActivityEducation; import com.kunzisoft.keepass.magikeyboard.KeyboardEntryNotificationService; import com.kunzisoft.keepass.magikeyboard.KeyboardHelper; import com.kunzisoft.keepass.magikeyboard.MagikIME; @@ -712,141 +716,6 @@ public class GroupActivity extends LockingActivity searchSuggestionAdapter.reInit(this); } - /** - * Check and display learning views - * Displays the explanation for a add, search, sort a new node and lock the database - */ - private void checkAndPerformedEducation(Menu menu) { - if (PreferencesUtil.isEducationScreensEnabled(this)) { - - // If no node, show education to add new one - if (listNodesFragment != null - && listNodesFragment.isEmpty()) { - if (!PreferencesUtil.isEducationNewNodePerformed(this) - && addNodeButtonView.isEnable()) { - - TapTargetView.showFor(this, - TapTarget.forView(findViewById(R.id.add_button), - getString(R.string.education_new_node_title), - getString(R.string.education_new_node_summary)) - .textColorInt(Color.WHITE) - .tintTarget(false) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - addNodeButtonView.openButtonIfClose(); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - } - }); - PreferencesUtil.saveEducationPreference(this, - R.string.education_new_node_key); - - } - } - // Else show the search education - else if (!PreferencesUtil.isEducationSearchPerformed(this)) { - - try { - TapTargetView.showFor(this, - TapTarget.forToolbarMenuItem(toolbar, R.id.menu_search, - getString(R.string.education_search_title), - getString(R.string.education_search_summary)) - .textColorInt(Color.WHITE) - .tintTarget(true) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - MenuItem searchItem = menu.findItem(R.id.menu_search); - searchItem.expandActionView(); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - } - }); - PreferencesUtil.saveEducationPreference(this, - R.string.education_search_key); - } catch (Exception e) { - // If icon not visible - Log.w(TAG, "Can't performed education for search"); - } - } - // Else show the sort education - else if (!PreferencesUtil.isEducationSortPerformed(this)) { - - try { - TapTargetView.showFor(this, - TapTarget.forToolbarMenuItem(toolbar, R.id.menu_sort, - getString(R.string.education_sort_title), - getString(R.string.education_sort_summary)) - .textColorInt(Color.WHITE) - .tintTarget(true) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - MenuItem sortItem = menu.findItem(R.id.menu_sort); - onOptionsItemSelected(sortItem); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - } - }); - PreferencesUtil.saveEducationPreference(this, - R.string.education_sort_key); - } catch (Exception e) { - Log.w(TAG, "Can't performed education for sort"); - } - } - // Else show the lock education - else if (!PreferencesUtil.isEducationLockPerformed(this)) { - - try { - TapTargetView.showFor(this, - TapTarget.forToolbarMenuItem(toolbar, R.id.menu_lock, - getString(R.string.education_lock_title), - getString(R.string.education_lock_summary)) - .textColorInt(Color.WHITE) - .tintTarget(true) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - MenuItem lockItem = menu.findItem(R.id.menu_lock); - onOptionsItemSelected(lockItem); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - } - }); - PreferencesUtil.saveEducationPreference(this, - R.string.education_lock_key); - } catch (Exception e) { - Log.w(TAG, "Can't performed education for lock"); - } - } - } - } - @Override protected void onStop() { super.onStop(); @@ -898,11 +767,63 @@ public class GroupActivity extends LockingActivity super.onCreateOptionsMenu(menu); // Launch education screen - new Handler().post(() -> checkAndPerformedEducation(menu)); + new Handler().post(() -> performedNextEducation(new GroupActivityEducation(this), menu)); return true; } + private void performedNextEducation(GroupActivityEducation groupActivityEducation, + Menu menu) { + // If no node, show education to add new one + if (listNodesFragment != null + && listNodesFragment.isEmpty() + && addNodeButtonView.isEnable() + && groupActivityEducation.checkAndPerformedAddNodeButtonEducation( + addNodeButtonView, + tapTargetView -> { + addNodeButtonView.openButtonIfClose(); + return null; + }, + tapTargetView -> { + performedNextEducation(groupActivityEducation, menu); + return null; + } + )); + else if (toolbar.findViewById(R.id.menu_search) != null + && groupActivityEducation.checkAndPerformedSearchMenuEducation( + toolbar.findViewById(R.id.menu_search), + tapTargetView -> { + menu.findItem(R.id.menu_search).expandActionView(); + return null; + }, + tapTargetView -> { + performedNextEducation(groupActivityEducation, menu); + return null; + })); + else if (toolbar.findViewById(R.id.menu_sort) != null + && groupActivityEducation.checkAndPerformedSortMenuEducation( + toolbar.findViewById(R.id.menu_sort), + tapTargetView -> { + onOptionsItemSelected(menu.findItem(R.id.menu_sort)); + return null; + }, + tapTargetView -> { + performedNextEducation(groupActivityEducation, menu); + return null; + })); + else if (toolbar.findViewById(R.id.menu_lock) != null + && groupActivityEducation.checkAndPerformedLockMenuEducation( + toolbar.findViewById(R.id.menu_lock), + tapTargetView -> { + onOptionsItemSelected(menu.findItem(R.id.menu_lock)); + return null; + }, + tapTargetView -> { + performedNextEducation(groupActivityEducation, menu); + return null; + })); + } + @Override public void startActivity(Intent intent) { diff --git a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt new file mode 100644 index 000000000..d39783e2b --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt @@ -0,0 +1,281 @@ +package com.kunzisoft.keepass.education + +import android.app.Activity +import android.content.Context +import android.content.SharedPreferences +import android.preference.PreferenceManager +import android.util.Log +import com.getkeepsafe.taptargetview.TapTarget +import com.getkeepsafe.taptargetview.TapTargetView +import com.kunzisoft.keepass.R + +open class Education(val activity: Activity) { + + /** + * Utility method to save preference after an education action + */ + protected fun checkAndPerformedEducation(isEducationAlreadyPerformed: Boolean, + tapTarget: TapTarget, + listener: TapTargetView.Listener, + saveEducationStringId: Int): Boolean { + var doEducation = false + if (isEducationScreensEnabled()) { + if (isEducationAlreadyPerformed) { + try { + TapTargetView.showFor(activity, tapTarget, listener) + saveEducationPreference(activity, saveEducationStringId) + doEducation = true + } catch (e: Exception) { + Log.w(Education::class.java.name, "Can't performed education " + e.message) + } + } + } + return doEducation + } + + + /** + * Define if educations screens are enabled + */ + fun isEducationScreensEnabled(): Boolean { + return isEducationScreensEnabled(activity) + } + + /** + * Register education preferences as true in EDUCATION_PREFERENCE SharedPreferences + * + * @param context The context to retrieve the key string in XML + * @param educationKeys Keys to save as boolean 'true' + */ + fun saveEducationPreference(context: Context, vararg educationKeys: Int) { + val sharedPreferences = getEducationSharedPreferences(context) + val editor = sharedPreferences.edit() + for (key in educationKeys) { + editor.putBoolean(context.getString(key), true) + } + editor.apply() + } + + companion object { + + private const val EDUCATION_PREFERENCE = "kdbxeducation" + + /** + * All preference keys associated with education + */ + val educationResourcesKeys = intArrayOf( + R.string.education_create_db_key, + R.string.education_select_db_key, + R.string.education_open_link_db_key, + R.string.education_unlock_key, + R.string.education_read_only_key, + R.string.education_fingerprint_key, + R.string.education_search_key, + R.string.education_new_node_key, + R.string.education_sort_key, + R.string.education_lock_key, + R.string.education_copy_username_key, + R.string.education_entry_edit_key, + R.string.education_password_generator_key, + R.string.education_entry_new_field_key) + + + /** + * Get preferences bundle for education + */ + fun getEducationSharedPreferences(ctx: Context): SharedPreferences { + return ctx.getSharedPreferences( + EDUCATION_PREFERENCE, + Context.MODE_PRIVATE) + } + + /** + * Define if educations screens are enabled + */ + fun isEducationScreensEnabled(context: Context): Boolean { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + return sharedPreferences.getBoolean(context.getString(R.string.enable_education_screens_key), + context.resources.getBoolean(R.bool.enable_education_screens_default)) + } + + /** + * Determines whether the explanatory view of the database creation has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_create_db_key key + */ + fun isEducationCreateDatabasePerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_create_db_key), + context.resources.getBoolean(R.bool.education_create_db_default)) + } + + /** + * Determines whether the explanatory view of the database selection has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_select_db_key key + */ + fun isEducationSelectDatabasePerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_select_db_key), + context.resources.getBoolean(R.bool.education_select_db_default)) + } + + /** + * Determines whether the explanatory view of the database selection has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_select_db_key key + */ + fun isEducationOpenLinkDatabasePerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_open_link_db_key), + context.resources.getBoolean(R.bool.education_open_link_db_default)) + } + + /** + * Determines whether the explanatory view of the database unlock has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_unlock_key key + */ + fun isEducationUnlockPerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_unlock_key), + context.resources.getBoolean(R.bool.education_unlock_default)) + } + + /** + * Determines whether the explanatory view of the database read-only has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_read_only_key key + */ + fun isEducationReadOnlyPerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_read_only_key), + context.resources.getBoolean(R.bool.education_read_only_default)) + } + + /** + * Determines whether the explanatory view of the fingerprint unlock has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_fingerprint_key key + */ + fun isEducationFingerprintPerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_fingerprint_key), + context.resources.getBoolean(R.bool.education_fingerprint_default)) + } + + /** + * Determines whether the explanatory view of search has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_search_key key + */ + fun isEducationSearchPerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_search_key), + context.resources.getBoolean(R.bool.education_search_default)) + } + + /** + * Determines whether the explanatory view of add new node has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_new_node_key key + */ + fun isEducationNewNodePerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_new_node_key), + context.resources.getBoolean(R.bool.education_new_node_default)) + } + + /** + * Determines whether the explanatory view of the sort has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_sort_key key + */ + fun isEducationSortPerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_sort_key), + context.resources.getBoolean(R.bool.education_sort_default)) + } + + /** + * Determines whether the explanatory view of the database lock has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_lock_key key + */ + fun isEducationLockPerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_lock_key), + context.resources.getBoolean(R.bool.education_lock_default)) + } + + /** + * Determines whether the explanatory view of the username copy has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_copy_username_key key + */ + fun isEducationCopyUsernamePerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_copy_username_key), + context.resources.getBoolean(R.bool.education_copy_username_key)) + } + + /** + * Determines whether the explanatory view of the entry edition has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_entry_edit_key key + */ + fun isEducationEntryEditPerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_entry_edit_key), + context.resources.getBoolean(R.bool.education_entry_edit_default)) + } + + /** + * Determines whether the explanatory view of the password generator has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_password_generator_key key + */ + fun isEducationPasswordGeneratorPerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_password_generator_key), + context.resources.getBoolean(R.bool.education_password_generator_default)) + } + + /** + * Determines whether the explanatory view of the new fields button in an entry has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_entry_new_field_key key + */ + fun isEducationEntryNewFieldPerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_entry_new_field_key), + context.resources.getBoolean(R.bool.education_entry_new_field_default)) + } + + /** + * Defines if the reset education preference has been reclicked + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_screen_reclicked_key key + */ + fun isEducationScreenReclickedPerformed(context: Context): Boolean { + val prefs = getEducationSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.education_screen_reclicked_key), + context.resources.getBoolean(R.bool.education_screen_reclicked_default)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt new file mode 100644 index 000000000..853e8b0e6 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt @@ -0,0 +1,69 @@ +package com.kunzisoft.keepass.education + +import android.app.Activity +import android.graphics.Color +import android.view.View +import com.getkeepsafe.taptargetview.TapTarget +import com.getkeepsafe.taptargetview.TapTargetView +import com.kunzisoft.keepass.R + + +class EntryActivityEducation(activity: Activity) + : Education(activity) { + + fun checkAndPerformedEntryCopyEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation( + !isEducationCopyUsernamePerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_field_copy_title), + activity.getString(R.string.education_field_copy_summary)) + .textColorInt(Color.WHITE) + .tintTarget(false) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_copy_username_key) + } + + /** + * Check and display learning views + * Displays the explanation for copying a field and editing an entry + */ + fun checkAndPerformedEntryEditEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation( + !isEducationEntryEditPerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_entry_edit_title), + activity.getString(R.string.education_entry_edit_summary)) + .textColorInt(Color.WHITE) + .tintTarget(true) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_entry_edit_key) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt new file mode 100644 index 000000000..ad4190a7f --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt @@ -0,0 +1,66 @@ +package com.kunzisoft.keepass.education + +import android.app.Activity +import android.graphics.Color +import android.view.View +import com.getkeepsafe.taptargetview.TapTarget +import com.getkeepsafe.taptargetview.TapTargetView +import com.kunzisoft.keepass.R + +class EntryEditActivityEducation(activity: Activity) + : Education(activity) { + + fun checkAndPerformedGeneratePasswordEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationPasswordGeneratorPerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_generate_password_title), + activity.getString(R.string.education_generate_password_summary)) + .textColorInt(Color.WHITE) + .tintTarget(false) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_password_generator_key) + } + + /** + * Check and display learning views + * Displays the explanation for the icon selection, the password generator and for a new field + */ + fun checkAndPerformedEntryNewFieldEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationEntryNewFieldPerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_entry_new_field_title), + activity.getString(R.string.education_entry_new_field_summary)) + .textColorInt(Color.WHITE) + .tintTarget(false) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_entry_new_field_key) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt new file mode 100644 index 000000000..975bbef0d --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt @@ -0,0 +1,102 @@ +package com.kunzisoft.keepass.education + +import android.app.Activity +import android.graphics.Color +import android.support.v4.content.ContextCompat +import android.view.View +import com.getkeepsafe.taptargetview.TapTarget +import com.getkeepsafe.taptargetview.TapTargetView +import com.kunzisoft.keepass.R + +class FileDatabaseSelectActivityEducation(activity: Activity) + : Education(activity) { + + /** + * Check and display learning views + * Displays the explanation for a database creation then a database selection + */ + fun checkAndPerformedCreateDatabaseEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + + // Try to open the creation base education + return checkAndPerformedEducation(!isEducationCreateDatabasePerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_create_database_title), + activity.getString(R.string.education_create_database_summary)) + .icon(ContextCompat.getDrawable(activity, R.drawable.ic_database_plus_white_24dp)) + .textColorInt(Color.WHITE) + .tintTarget(true) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_create_db_key) + } + + /** + * Check and display learning views + * Displays the explanation for a database selection + */ + fun checkAndPerformedSelectDatabaseEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationSelectDatabasePerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_select_database_title), + activity.getString(R.string.education_select_database_summary)) + .icon(ContextCompat.getDrawable(activity, R.drawable.ic_folder_white_24dp)) + .textColorInt(Color.WHITE) + .tintTarget(true) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_select_db_key) + } + + + fun checkAndPerformedOpenLinkDatabaseEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationOpenLinkDatabasePerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_open_link_database_title), + activity.getString(R.string.education_open_link_database_summary)) + .icon(ContextCompat.getDrawable(activity, R.drawable.ic_link_white_24dp)) + .textColorInt(Color.WHITE) + .tintTarget(true) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_open_link_db_key) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt new file mode 100644 index 000000000..e4cde8545 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt @@ -0,0 +1,112 @@ +package com.kunzisoft.keepass.education + +import android.app.Activity +import android.graphics.Color +import android.view.View +import com.getkeepsafe.taptargetview.TapTarget +import com.getkeepsafe.taptargetview.TapTargetView +import com.kunzisoft.keepass.R + +class GroupActivityEducation(activity: Activity) + : Education(activity) { + + fun checkAndPerformedAddNodeButtonEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationNewNodePerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_new_node_title), + activity.getString(R.string.education_new_node_summary)) + .textColorInt(Color.WHITE) + .tintTarget(false) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_new_node_key) + } + + fun checkAndPerformedSearchMenuEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationSearchPerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_search_title), + activity.getString(R.string.education_search_summary)) + .textColorInt(Color.WHITE) + .tintTarget(true) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_search_key) + } + + fun checkAndPerformedSortMenuEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationSortPerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_sort_title), + activity.getString(R.string.education_sort_summary)) + .textColorInt(Color.WHITE) + .tintTarget(true) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_sort_key) + } + + fun checkAndPerformedLockMenuEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationLockPerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_lock_title), + activity.getString(R.string.education_lock_summary)) + .textColorInt(Color.WHITE) + .tintTarget(true) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_lock_key) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt new file mode 100644 index 000000000..ab425fe0c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt @@ -0,0 +1,90 @@ +package com.kunzisoft.keepass.education + +import android.app.Activity +import android.graphics.Color +import android.support.v4.content.ContextCompat +import android.view.View +import com.getkeepsafe.taptargetview.TapTarget +import com.getkeepsafe.taptargetview.TapTargetView +import com.kunzisoft.keepass.R + +class PasswordActivityEducation(activity: Activity) + : Education(activity) { + + fun checkAndPerformedFingerprintUnlockEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationUnlockPerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_unlock_title), + activity.getString(R.string.education_unlock_summary)) + .dimColor(R.color.green) + .icon(ContextCompat.getDrawable(activity, R.mipmap.ic_launcher_round)) + .textColorInt(Color.WHITE) + .tintTarget(false) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_unlock_key) + } + + fun checkAndPerformedReadOnlyEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationReadOnlyPerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_read_only_title), + activity.getString(R.string.education_read_only_summary)) + .textColorInt(Color.WHITE) + .tintTarget(true) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_read_only_key) + } + + fun checkAndPerformedFingerprintEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationFingerprintPerformed(activity), + TapTarget.forView(educationView, + activity.getString(R.string.education_fingerprint_title), + activity.getString(R.string.education_fingerprint_summary)) + .textColorInt(Color.WHITE) + .tintTarget(false) + .cancelable(true), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + onEducationViewClick?.invoke(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + super.onOuterCircleClick(view) + view?.dismiss(false) + onOuterViewClick?.invoke(view) + } + }, + R.string.education_fingerprint_key) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java index 1ee943508..08007f6ef 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java @@ -24,15 +24,14 @@ import android.app.Activity; import android.app.assist.AssistStructure; import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; +import android.os.Handler; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; -import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -45,23 +44,22 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; -import com.getkeepsafe.taptargetview.TapTarget; -import com.getkeepsafe.taptargetview.TapTargetView; import com.kunzisoft.keepass.R; import com.kunzisoft.keepass.activities.EntrySelectionHelper; import com.kunzisoft.keepass.activities.GroupActivity; import com.kunzisoft.keepass.autofill.AutofillHelper; import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable; import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable; -import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory; -import com.kunzisoft.keepass.tasks.ActionRunnable; import com.kunzisoft.keepass.database.action.ProgressDialogRunnable; import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment; import com.kunzisoft.keepass.dialogs.CreateFileDialogFragment; +import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation; +import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory; import com.kunzisoft.keepass.magikeyboard.KeyboardHelper; import com.kunzisoft.keepass.password.PasswordActivity; import com.kunzisoft.keepass.settings.PreferencesUtil; import com.kunzisoft.keepass.stylish.StylishActivity; +import com.kunzisoft.keepass.tasks.ActionRunnable; import com.kunzisoft.keepass.utils.EmptyUtils; import com.kunzisoft.keepass.utils.MenuUtil; import com.kunzisoft.keepass.utils.UriUtil; @@ -233,8 +231,40 @@ public class FileDatabaseSelectActivity extends StylishActivity implements } } - // For the first time show education - checkAndPerformedEducation(); + new Handler().post(() -> performedNextEducation(new FileDatabaseSelectActivityEducation(this))); + } + + private void performedNextEducation(FileDatabaseSelectActivityEducation fileDatabaseSelectActivityEducation) { + // If no recent files + if (!fileDatabaseHistory.hasRecentFiles() + && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation( + createButtonView, + tapTargetView -> { + FileDatabaseSelectActivityPermissionsDispatcher + .openCreateFileDialogFragmentWithPermissionCheck(FileDatabaseSelectActivity.this); + return null; + }, + tapTargetView -> { + // But if the user cancel, it can also select a database + performedNextEducation(fileDatabaseSelectActivityEducation); + return null; + }) + ); + else if (fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( + browseButtonView, + tapTargetView -> { + keyFileHelper.getOpenFileOnClickViewListener().onClick(tapTargetView); + return null; + }, + tapTargetView -> { + fileDatabaseSelectActivityEducation.checkAndPerformedOpenLinkDatabaseEducation( + fileSelectExpandableButton, + tapTargetView1 -> null, + tapTargetView12 -> null + ); + return null; + } + )); } private void fileNoFoundAction(FileNotFoundException e) { @@ -314,113 +344,6 @@ public class FileDatabaseSelectActivity extends StylishActivity implements mAdapter.notifyDataSetChanged(); } - /** - * Check and display learning views - * Displays the explanation for a database creation then a database selection - */ - private void checkAndPerformedEducation() { - - // If no recent files - if ( !fileDatabaseHistory.hasRecentFiles() ) { - // Try to open the creation base education - if (!PreferencesUtil.isEducationCreateDatabasePerformed(this) ) { - - TapTargetView.showFor(this, - TapTarget.forView(createButtonView, - getString(R.string.education_create_database_title), - getString(R.string.education_create_database_summary)) - .icon(ContextCompat.getDrawable(this, R.drawable.ic_database_plus_white_24dp)) - .textColorInt(Color.WHITE) - .tintTarget(true) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - FileDatabaseSelectActivityPermissionsDispatcher - .openCreateFileDialogFragmentWithPermissionCheck(FileDatabaseSelectActivity.this); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - // But if the user cancel, it can also select a database - checkAndPerformedEducationForSelection(); - } - }); - PreferencesUtil.saveEducationPreference(FileDatabaseSelectActivity.this, - R.string.education_create_db_key); - } - } - else - checkAndPerformedEducationForSelection(); - } - - /** - * Check and display learning views - * Displays the explanation for a database selection - */ - private void checkAndPerformedEducationForSelection() { - if (PreferencesUtil.isEducationScreensEnabled(this)) { - - if (!PreferencesUtil.isEducationSelectDatabasePerformed(this) - && browseButtonView != null) { - - TapTargetView.showFor(FileDatabaseSelectActivity.this, - TapTarget.forView(browseButtonView, - getString(R.string.education_select_database_title), - getString(R.string.education_select_database_summary)) - .icon(ContextCompat.getDrawable(this, R.drawable.ic_folder_white_24dp)) - .textColorInt(Color.WHITE) - .tintTarget(true) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - keyFileHelper.getOpenFileOnClickViewListener().onClick(view); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - - if (!PreferencesUtil.isEducationOpenLinkDatabasePerformed(FileDatabaseSelectActivity.this)) { - - TapTargetView.showFor(FileDatabaseSelectActivity.this, - TapTarget.forView(fileSelectExpandableButton, - getString(R.string.education_open_link_database_title), - getString(R.string.education_open_link_database_summary)) - .icon(ContextCompat.getDrawable(FileDatabaseSelectActivity.this, R.drawable.ic_link_white_24dp)) - .textColorInt(Color.WHITE) - .tintTarget(true) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - // Do nothing here - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - } - }); - PreferencesUtil.saveEducationPreference(FileDatabaseSelectActivity.this, - R.string.education_open_link_db_key); - } - } - }); - PreferencesUtil.saveEducationPreference(FileDatabaseSelectActivity.this, - R.string.education_select_db_key); - } - } - } - @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); diff --git a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java index 4bcc6c778..685898e4f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java @@ -25,7 +25,6 @@ import android.app.assist.AssistStructure; import android.app.backup.BackupManager; import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.Color; import android.hardware.fingerprint.FingerprintManager; import android.net.Uri; import android.os.Build; @@ -34,7 +33,6 @@ import android.os.Handler; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; -import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; import android.text.Editable; @@ -44,9 +42,13 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.widget.*; -import com.getkeepsafe.taptargetview.TapTarget; -import com.getkeepsafe.taptargetview.TapTargetView; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + import com.kunzisoft.keepass.R; import com.kunzisoft.keepass.activities.EntrySelectionHelper; import com.kunzisoft.keepass.activities.GroupActivity; @@ -59,6 +61,7 @@ import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable; import com.kunzisoft.keepass.database.action.ProgressDialogRunnable; import com.kunzisoft.keepass.database.element.Database; import com.kunzisoft.keepass.dialogs.PasswordEncodingDialogHelper; +import com.kunzisoft.keepass.education.PasswordActivityEducation; import com.kunzisoft.keepass.fileselect.KeyFileHelper; import com.kunzisoft.keepass.fingerprint.FingerPrintAnimatedVector; import com.kunzisoft.keepass.fingerprint.FingerPrintExplanationDialog; @@ -70,13 +73,20 @@ import com.kunzisoft.keepass.tasks.ActionRunnable; import com.kunzisoft.keepass.utils.EmptyUtils; import com.kunzisoft.keepass.utils.MenuUtil; import com.kunzisoft.keepass.utils.UriUtil; + import org.jetbrains.annotations.Nullable; -import permissions.dispatcher.*; import java.io.File; import java.io.FileNotFoundException; import java.lang.ref.WeakReference; +import permissions.dispatcher.NeedsPermission; +import permissions.dispatcher.OnNeverAskAgain; +import permissions.dispatcher.OnPermissionDenied; +import permissions.dispatcher.OnShowRationale; +import permissions.dispatcher.PermissionRequest; +import permissions.dispatcher.RuntimePermissions; + @RuntimePermissions public class PasswordActivity extends StylishActivity implements FingerPrintHelper.FingerPrintCallback, UriIntentInitTaskCallback { @@ -325,113 +335,6 @@ public class PasswordActivity extends StylishActivity super.onSaveInstanceState(outState); } - /** - * Check and display learning views - * Displays the explanation for a database opening with fingerprints if available - */ - private void checkAndPerformedEducation(Menu menu) { - if (PreferencesUtil.isEducationScreensEnabled(this)) { - - if (!PreferencesUtil.isEducationUnlockPerformed(this)) { - - TapTargetView.showFor(this, - TapTarget.forView(findViewById(R.id.password_input_container), - getString(R.string.education_unlock_title), - getString(R.string.education_unlock_summary)) - .dimColor(R.color.green) - .icon(ContextCompat.getDrawable(getApplicationContext(), R.mipmap.ic_launcher_round)) - .textColorInt(Color.WHITE) - .tintTarget(false) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - performedReadOnlyEducation(menu); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - performedReadOnlyEducation(menu); - - } - }); - // TODO make a period for donation - PreferencesUtil.saveEducationPreference(PasswordActivity.this, - R.string.education_unlock_key); - } - } - } - - /** - * Check and display learning views - * Displays read-only if available - */ - private void performedReadOnlyEducation(Menu menu) { - if (!PreferencesUtil.isEducationReadOnlyPerformed(this)) { - try { - TapTargetView.showFor(this, - TapTarget.forToolbarMenuItem(toolbar, R.id.menu_open_file_read_mode_key, - getString(R.string.education_read_only_title), - getString(R.string.education_read_only_summary)) - .textColorInt(Color.WHITE) - .tintTarget(true) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - MenuItem editItem = menu.findItem(R.id.menu_open_file_read_mode_key); - onOptionsItemSelected(editItem); - checkAndPerformedEducationForFingerprint(); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - checkAndPerformedEducationForFingerprint(); - } - }); - PreferencesUtil.saveEducationPreference(this, - R.string.education_read_only_key); - } catch (Exception e) { - // If icon not visible - Log.w(TAG, "Can't performed education for entry's edition"); - } - } - } - - /** - * Check and display learning views - * Displays fingerprints if available - */ - private void checkAndPerformedEducationForFingerprint() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - - if ( PreferencesUtil.isFingerprintEnable(getApplicationContext()) - && FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager.class))) { - - TapTargetView.showFor(this, - TapTarget.forView(fingerprintImageView, - getString(R.string.education_fingerprint_title), - getString(R.string.education_fingerprint_summary)) - .textColorInt(Color.WHITE) - .tintTarget(false) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - } - }); - } - } - } - @Override public void onPostInitTask(Uri dbUri, Uri keyFileUri, Integer errorStringId) { mDbUri = dbUri; @@ -957,11 +860,46 @@ public class PasswordActivity extends StylishActivity super.onCreateOptionsMenu(menu); // Show education views - new Handler().post(() -> checkAndPerformedEducation(menu)); + new Handler().post(() -> performedNextEducation(new PasswordActivityEducation(this), menu)); return true; } + private void performedNextEducation(PasswordActivityEducation passwordActivityEducation, + Menu menu) { + if (passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation( + toolbar, + tapTargetView -> { + performedNextEducation(passwordActivityEducation, menu); + return null; + }, + tapTargetView -> { + performedNextEducation(passwordActivityEducation, menu); + return null; + }) + ); + else if (toolbar.findViewById(R.id.menu_open_file_read_mode_key) != null + && passwordActivityEducation.checkAndPerformedReadOnlyEducation( + toolbar.findViewById(R.id.menu_open_file_read_mode_key), + tapTargetView -> { + onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key)); + performedNextEducation(passwordActivityEducation, menu); + return null; + }, + tapTargetView -> { + performedNextEducation(passwordActivityEducation, menu); + return null; + }) + ); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && PreferencesUtil.isFingerprintEnable(getApplicationContext()) + && FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager.class)) + && passwordActivityEducation.checkAndPerformedFingerprintEducation(fingerprintImageView, + tapTargetView -> null, + tapTargetView -> null) + ); + } + private void changeOpenFileReadIcon(MenuItem togglePassword) { if ( readOnly ) { togglePassword.setTitle(R.string.menu_file_selection_read_only); diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java index e9e4cea73..768370de8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java @@ -49,6 +49,7 @@ import com.kunzisoft.keepass.database.element.Database; import com.kunzisoft.keepass.dialogs.ProFeatureDialogFragment; import com.kunzisoft.keepass.dialogs.UnavailableFeatureDialogFragment; import com.kunzisoft.keepass.dialogs.UnderDevelopmentFeatureDialogFragment; +import com.kunzisoft.keepass.education.Education; import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory; import com.kunzisoft.keepass.fingerprint.FingerPrintHelper; import com.kunzisoft.keepass.icons.IconPackChooser; @@ -392,7 +393,7 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat Preference stylePreference = findPreference(getString(R.string.setting_style_key)); stylePreference.setOnPreferenceChangeListener((preference, newValue) -> { String styleIdString = (String) newValue; - if (!(!BuildConfig.CLOSED_STORE && PreferencesUtil.isEducationScreenReclickedPerformed(getContext()))) + if (!(!BuildConfig.CLOSED_STORE && Education.Companion.isEducationScreenReclickedPerformed(getContext()))) for (String themeIdDisabled : BuildConfig.STYLES_DISABLED) { if (themeIdDisabled.equals(styleIdString)) { ProFeatureDialogFragment dialogFragment = new ProFeatureDialogFragment(); @@ -411,7 +412,7 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat Preference iconPackPreference = findPreference(getString(R.string.setting_icon_pack_choose_key)); iconPackPreference.setOnPreferenceChangeListener((preference, newValue) -> { String iconPackId = (String) newValue; - if (!(!BuildConfig.CLOSED_STORE && PreferencesUtil.isEducationScreenReclickedPerformed(getContext()))) + if (!(!BuildConfig.CLOSED_STORE && Education.Companion.isEducationScreenReclickedPerformed(getContext()))) for (String iconPackIdDisabled : BuildConfig.ICON_PACKS_DISABLED) { if (iconPackIdDisabled.equals(iconPackId)) { ProFeatureDialogFragment dialogFragment = new ProFeatureDialogFragment(); @@ -429,9 +430,9 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat resetEducationScreens.setOnPreferenceClickListener(preference -> { // To allow only one toast if (count == 0) { - SharedPreferences sharedPreferences = PreferencesUtil.getEducationSharedPreferences(getContext()); + SharedPreferences sharedPreferences = Education.Companion.getEducationSharedPreferences(getContext()); SharedPreferences.Editor editor = sharedPreferences.edit(); - for (int resourceId : PreferencesUtil.educationResourceKeys) { + for (int resourceId : Education.Companion.getEducationResourcesKeys()) { editor.putBoolean(getString(resourceId), false); } editor.apply(); @@ -487,7 +488,7 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat super.onStop(); if(count==10) { if (getActivity()!=null) - PreferencesUtil.getEducationSharedPreferences(getActivity()).edit() + Education.Companion.getEducationSharedPreferences(getActivity()).edit() .putBoolean(getString(R.string.education_screen_reclicked_key), true).apply(); } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.java b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.java index f1426f7d3..590ec481a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.java +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.java @@ -33,7 +33,6 @@ import java.util.Set; public class PreferencesUtil { private static final String NO_BACKUP_PREFERENCE_FILE_NAME = "nobackup"; - private static final String EDUCATION_PREFERENCE = "kdbxeducation"; public static SharedPreferences getNoBackupSharedPreferences(Context ctx) { return ctx.getSharedPreferences( @@ -41,12 +40,6 @@ public class PreferencesUtil { Context.MODE_PRIVATE); } - public static SharedPreferences getEducationSharedPreferences(Context ctx) { - return ctx.getSharedPreferences( - PreferencesUtil.EDUCATION_PREFERENCE, - Context.MODE_PRIVATE); - } - public static void deleteAllValuesFromNoBackupPreferences(Context ctx) { SharedPreferences prefsNoBackup = getNoBackupSharedPreferences(ctx); SharedPreferences.Editor sharedPreferencesEditor = prefsNoBackup.edit(); @@ -220,212 +213,4 @@ public class PreferencesUtil { return prefs.getBoolean(context.getString(R.string.keyboard_key_sound_key), context.getResources().getBoolean(R.bool.keyboard_key_sound_default)); } - - /** - * All preference keys associated with education - */ - public static int[] educationResourceKeys = new int[] { - R.string.education_create_db_key, - R.string.education_select_db_key, - R.string.education_open_link_db_key, - R.string.education_unlock_key, - R.string.education_read_only_key, - R.string.education_search_key, - R.string.education_new_node_key, - R.string.education_sort_key, - R.string.education_lock_key, - R.string.education_copy_username_key, - R.string.education_entry_edit_key, - R.string.education_password_generator_key, - R.string.education_entry_new_field_key - }; - - public static boolean isEducationScreensEnabled(Context context) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - return sharedPreferences.getBoolean(context.getString(R.string.enable_education_screens_key), - context.getResources().getBoolean(R.bool.enable_education_screens_default)); - } - - /** - * Register education preferences as true in EDUCATION_PREFERENCE SharedPreferences - * - * @param context The context to retrieve the key string in XML - * @param educationKeys Keys to save as boolean 'true' - */ - public static void saveEducationPreference(Context context, int... educationKeys) { - SharedPreferences sharedPreferences = PreferencesUtil.getEducationSharedPreferences(context); - SharedPreferences.Editor editor = sharedPreferences.edit(); - for (int key : educationKeys) { - editor.putBoolean(context.getString(key), true); - } - editor.apply(); - } - - /** - * Determines whether the explanatory view of the database creation has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_create_db_key key - */ - public static boolean isEducationCreateDatabasePerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_create_db_key), - context.getResources().getBoolean(R.bool.education_create_db_default)); - } - - /** - * Determines whether the explanatory view of the database selection has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_select_db_key key - */ - public static boolean isEducationSelectDatabasePerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_select_db_key), - context.getResources().getBoolean(R.bool.education_select_db_default)); - } - - /** - * Determines whether the explanatory view of the database selection has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_select_db_key key - */ - public static boolean isEducationOpenLinkDatabasePerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_open_link_db_key), - context.getResources().getBoolean(R.bool.education_open_link_db_default)); - } - - /** - * Determines whether the explanatory view of the database unlock has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_unlock_key key - */ - public static boolean isEducationUnlockPerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_unlock_key), - context.getResources().getBoolean(R.bool.education_unlock_default)); - } - - /** - * Determines whether the explanatory view of the database read-only has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_read_only_key key - */ - public static boolean isEducationReadOnlyPerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_read_only_key), - context.getResources().getBoolean(R.bool.education_read_only_default)); - } - - /** - * Determines whether the explanatory view of search has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_search_key key - */ - public static boolean isEducationSearchPerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_search_key), - context.getResources().getBoolean(R.bool.education_search_default)); - } - - /** - * Determines whether the explanatory view of add new node has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_new_node_key key - */ - public static boolean isEducationNewNodePerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_new_node_key), - context.getResources().getBoolean(R.bool.education_new_node_default)); - } - - /** - * Determines whether the explanatory view of the sort has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_sort_key key - */ - public static boolean isEducationSortPerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_sort_key), - context.getResources().getBoolean(R.bool.education_sort_default)); - } - - /** - * Determines whether the explanatory view of the database lock has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_lock_key key - */ - public static boolean isEducationLockPerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_lock_key), - context.getResources().getBoolean(R.bool.education_lock_default)); - } - - /** - * Determines whether the explanatory view of the username copy has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_copy_username_key key - */ - public static boolean isEducationCopyUsernamePerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_copy_username_key), - context.getResources().getBoolean(R.bool.education_copy_username_key)); - } - - /** - * Determines whether the explanatory view of the entry edition has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_entry_edit_key key - */ - public static boolean isEducationEntryEditPerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_entry_edit_key), - context.getResources().getBoolean(R.bool.education_entry_edit_default)); - } - - /** - * Determines whether the explanatory view of the password generator has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_password_generator_key key - */ - public static boolean isEducationPasswordGeneratorPerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_password_generator_key), - context.getResources().getBoolean(R.bool.education_password_generator_default)); - } - - /** - * Determines whether the explanatory view of the new fields button in an entry has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_entry_new_field_key key - */ - public static boolean isEducationEntryNewFieldPerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_entry_new_field_key), - context.getResources().getBoolean(R.bool.education_entry_new_field_default)); - } - - /** - * Defines if the reset education preference has been reclicked - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_screen_reclicked_key key - */ - public static boolean isEducationScreenReclickedPerformed(Context context) { - SharedPreferences prefs = getEducationSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.education_screen_reclicked_key), - context.getResources().getBoolean(R.bool.education_screen_reclicked_default)); - } } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 4155e7112..c39912a4a 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -181,6 +181,8 @@ false education_read_only_key false + education_fingerprint_key + false education_search_key false education_new_node_key From 7dfe85450d1ff5ac361ef88bac4f51c1daffbb9b Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 7 Jul 2019 16:37:57 +0200 Subject: [PATCH 06/24] Kotlinized EntryActivity --- .../keepass/activities/EntryActivity.java | 484 ------------------ .../keepass/activities/EntryActivity.kt | 389 ++++++++++++++ .../keepass/activities/GroupActivity.java | 2 +- .../NotificationEntryCopyManager.kt | 90 ++++ .../keepass/view/EntryContentsView.java | 253 --------- .../keepass/view/EntryContentsView.kt | 250 +++++++++ 6 files changed, 730 insertions(+), 738 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/notifications/NotificationEntryCopyManager.kt delete mode 100644 app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java deleted file mode 100644 index 09a27ea44..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java +++ /dev/null @@ -1,484 +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.activities; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.Toolbar; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.activities.lock.LockingHideActivity; -import com.kunzisoft.keepass.app.App; -import com.kunzisoft.keepass.database.element.Database; -import com.kunzisoft.keepass.database.element.EntryVersioned; -import com.kunzisoft.keepass.database.element.PwNodeId; -import com.kunzisoft.keepass.database.element.security.ProtectedString; -import com.kunzisoft.keepass.education.EntryActivityEducation; -import com.kunzisoft.keepass.notifications.NotificationCopyingService; -import com.kunzisoft.keepass.notifications.NotificationField; -import com.kunzisoft.keepass.settings.PreferencesUtil; -import com.kunzisoft.keepass.settings.SettingsAutofillActivity; -import com.kunzisoft.keepass.timeout.ClipboardHelper; -import com.kunzisoft.keepass.timeout.TimeoutHelper; -import com.kunzisoft.keepass.utils.EmptyUtils; -import com.kunzisoft.keepass.utils.MenuUtil; -import com.kunzisoft.keepass.utils.Util; -import com.kunzisoft.keepass.view.EntryContentsView; - -import java.util.ArrayList; -import java.util.Date; - -import kotlin.Unit; -import kotlin.jvm.functions.Function2; - -import static com.kunzisoft.keepass.settings.PreferencesUtil.isClipboardNotificationsEnable; -import static com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields; - -public class EntryActivity extends LockingHideActivity { - private final static String TAG = EntryActivity.class.getName(); - - public static final String KEY_ENTRY = "entry"; - - private ImageView titleIconView; - private TextView titleView; - private EntryContentsView entryContentsView; - private Toolbar toolbar; - - protected EntryVersioned mEntry; - private boolean mShowPassword; - - private ClipboardHelper clipboardHelper; - private boolean firstLaunchOfActivity; - - private int iconColor; - - public static void launch(Activity activity, EntryVersioned pw, boolean readOnly) { - if (TimeoutHelper.INSTANCE.checkTimeAndLockIfTimeout(activity)) { - Intent intent = new Intent(activity, EntryActivity.class); - intent.putExtra(KEY_ENTRY, pw.getNodeId()); - ReadOnlyHelper.INSTANCE.putReadOnlyInIntent(intent, readOnly); - activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.entry_view); - - toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - assert getSupportActionBar() != null; - getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setDisplayShowHomeEnabled(true); - - Database db = App.Companion.getCurrentDatabase(); - setReadOnly(db.isReadOnly() || getReadOnly()); - - mShowPassword = !PreferencesUtil.isPasswordMask(this); - - // Get Entry from UUID - Intent i = getIntent(); - PwNodeId keyEntry; - try { - keyEntry = i.getParcelableExtra(KEY_ENTRY); - mEntry = db.getEntryById(keyEntry); - } catch (ClassCastException e) { - Log.e(TAG, "Unable to retrieve the entry key"); - } - if (mEntry == null) { - Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show(); - finish(); - return; - } - - // Update last access time. - mEntry.touch(false, false); - - // Retrieve the textColor to tint the icon - int[] attrs = {R.attr.textColorInverse}; - TypedArray ta = getTheme().obtainStyledAttributes(attrs); - iconColor = ta.getColor(0, Color.WHITE); - - // Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set - invalidateOptionsMenu(); - - // Get views - titleIconView = findViewById(R.id.entry_icon); - titleView = findViewById(R.id.entry_title); - entryContentsView = findViewById(R.id.entry_contents); - entryContentsView.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)); - - // Init the clipboard helper - clipboardHelper = new ClipboardHelper(this); - firstLaunchOfActivity = true; - } - - @Override - protected void onResume() { - super.onResume(); - - // Fill data in resume to update from EntryEditActivity - fillData(); - invalidateOptionsMenu(); - - Database database = App.Companion.getCurrentDatabase(); - // Start to manage field reference to copy a value from ref - database.startManageEntry(mEntry); - - boolean containsUsernameToCopy = - mEntry.getUsername().length() > 0; - boolean containsPasswordToCopy = - (mEntry.getPassword().length() > 0 - && PreferencesUtil.allowCopyPasswordAndProtectedFields(this)); - boolean containsExtraFieldToCopy = - (mEntry.allowExtraFields() - && ((mEntry.containsCustomFields() - && mEntry.containsCustomFieldsNotProtected()) - || (mEntry.containsCustomFields() - && mEntry.containsCustomFieldsProtected() - && PreferencesUtil.allowCopyPasswordAndProtectedFields(this)) - ) - ); - - // If notifications enabled in settings - // Don't if application timeout - if (firstLaunchOfActivity && isClipboardNotificationsEnable(getApplicationContext())) { - if (containsUsernameToCopy - || containsPasswordToCopy - || containsExtraFieldToCopy - ) { - // username already copied, waiting for user's action before copy password. - Intent intent = new Intent(this, NotificationCopyingService.class); - intent.setAction(NotificationCopyingService.ACTION_NEW_NOTIFICATION); - if (mEntry.getTitle() != null) - intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE, mEntry.getTitle()); - // Construct notification fields - ArrayList notificationFields = new ArrayList<>(); - // Add username if exists to notifications - if (containsUsernameToCopy) - notificationFields.add( - new NotificationField( - NotificationField.NotificationFieldId.USERNAME, - mEntry.getUsername(), - getResources())); - // Add password to notifications - if (containsPasswordToCopy) { - notificationFields.add( - new NotificationField( - NotificationField.NotificationFieldId.PASSWORD, - mEntry.getPassword(), - getResources())); - } - // Add extra fields - if (containsExtraFieldToCopy) { - try { - mEntry.getFields().doActionToAllCustomProtectedField(new Function2() { - private int anonymousFieldNumber = 0; - @Override - public Unit invoke(String key, ProtectedString value) { - //If value is not protected or allowed - if (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this)) { - notificationFields.add( - new NotificationField( - NotificationField.NotificationFieldId.getAnonymousFieldId()[anonymousFieldNumber], - value.toString(), - key, - getResources())); - anonymousFieldNumber++; - } - return null; - } - }); - } catch (ArrayIndexOutOfBoundsException e) { - Log.w(TAG, "Only " + NotificationField.NotificationFieldId.getAnonymousFieldId().length + - " anonymous notifications are available"); - } - } - // Add notifications - intent.putParcelableArrayListExtra(NotificationCopyingService.EXTRA_FIELDS, notificationFields); - - startService(intent); - } - - database.stopManageEntry(mEntry); - } - firstLaunchOfActivity = false; - } - - protected void fillData() { - Database database = App.Companion.getCurrentDatabase(); - database.startManageEntry(mEntry); - // Assign title icon - database.getDrawFactory().assignDatabaseIconTo(this, titleIconView, mEntry.getIcon(), iconColor); - - // Assign title text - titleView.setText(mEntry.getVisualTitle()); - - // Assign basic fields - entryContentsView.assignUserName(mEntry.getUsername()); - entryContentsView.assignUserNameCopyListener(view -> - clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(), - getString(R.string.copy_field, getString(R.string.entry_user_name))) - ); - - boolean allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this); - entryContentsView.assignPassword(mEntry.getPassword(), allowCopyPassword); - if (allowCopyPassword) { - entryContentsView.assignPasswordCopyListener(view -> - clipboardHelper.timeoutCopyToClipboard(mEntry.getPassword(), - getString(R.string.copy_field, getString(R.string.entry_password))) - ); - } else { - // If dialog not already shown - if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) { - entryContentsView.assignPasswordCopyListener(v -> { - String message = getString(R.string.allow_copy_password_warning) + - "\n\n" + - getString(R.string.clipboard_warning); - AlertDialog warningDialog = new AlertDialog.Builder(EntryActivity.this) - .setMessage(message).create(); - warningDialog.setButton(AlertDialog.BUTTON1, getText(android.R.string.ok), - (dialog, which) -> { - PreferencesUtil.setAllowCopyPasswordAndProtectedFields(EntryActivity.this, true); - dialog.dismiss(); - fillData(); - }); - warningDialog.setButton(AlertDialog.BUTTON2, getText(android.R.string.cancel), - (dialog, which) -> { - PreferencesUtil.setAllowCopyPasswordAndProtectedFields(EntryActivity.this, false); - dialog.dismiss(); - fillData(); - }); - warningDialog.show(); - }); - } else { - entryContentsView.assignPasswordCopyListener(null); - } - } - - entryContentsView.assignURL(mEntry.getUrl()); - - entryContentsView.setHiddenPasswordStyle(!mShowPassword); - entryContentsView.assignComment(mEntry.getNotes()); - - // Assign custom fields - if (mEntry.allowExtraFields()) { - entryContentsView.clearExtraFields(); - - mEntry.getFields().doActionToAllCustomProtectedField((label, value) -> { - boolean showAction = (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this)); - entryContentsView.addExtraField(label, value, showAction, view -> - clipboardHelper.timeoutCopyToClipboard( - value.toString(), - getString(R.string.copy_field, label) - ) - ); - return null; - }); - } - - // Assign dates - entryContentsView.assignCreationDate(mEntry.getCreationTime().getDate()); - entryContentsView.assignModificationDate(mEntry.getLastModificationTime().getDate()); - entryContentsView.assignLastAccessDate(mEntry.getLastAccessTime().getDate()); - Date expires = mEntry.getExpiryTime().getDate(); - if ( mEntry.isExpires() ) { - entryContentsView.assignExpiresDate(expires); - } else { - entryContentsView.assignExpiresDate(getString(R.string.never)); - } - - database.stopManageEntry(mEntry); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - switch (requestCode) { - case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE: - // Not directly get the entry from intent data but from database - fillData(); - break; - } - } - - private void changeShowPasswordIcon(MenuItem togglePassword) { - if ( mShowPassword ) { - togglePassword.setTitle(R.string.menu_hide_password); - togglePassword.setIcon(R.drawable.ic_visibility_off_white_24dp); - } else { - togglePassword.setTitle(R.string.menu_showpass); - togglePassword.setIcon(R.drawable.ic_visibility_white_24dp); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - - MenuInflater inflater = getMenuInflater(); - MenuUtil.INSTANCE.contributionMenuInflater(inflater, menu); - inflater.inflate(R.menu.entry, menu); - inflater.inflate(R.menu.database_lock, menu); - - if (getReadOnly()) { - MenuItem edit = menu.findItem(R.id.menu_edit); - if (edit != null) - edit.setVisible(false); - } - - MenuItem togglePassword = menu.findItem(R.id.menu_toggle_pass); - if (entryContentsView != null && togglePassword != null) { - if (entryContentsView.isPasswordPresent() || entryContentsView.atLeastOneFieldProtectedPresent()) { - changeShowPasswordIcon(togglePassword); - } else { - togglePassword.setVisible(false); - } - } - - MenuItem gotoUrl = menu.findItem(R.id.menu_goto_url); - if (gotoUrl != null) { - // In API >= 11 onCreateOptionsMenu may be called before onCreate completes - // so mEntry may not be set - if (mEntry == null) { - gotoUrl.setVisible(false); - } else { - String url = mEntry.getUrl(); - if (EmptyUtils.INSTANCE.isNullOrEmpty(url)) { - // disable button if url is not available - gotoUrl.setVisible(false); - } - } - } - - // Show education views - new Handler().post(() -> performedNextEducation(new EntryActivityEducation(this), menu)); - - return true; - } - - private void performedNextEducation(EntryActivityEducation entryActivityEducation, - Menu menu) { - if (entryContentsView != null - && entryContentsView.isUserNamePresent() - && entryActivityEducation.checkAndPerformedEntryCopyEducation( - findViewById(R.id.entry_user_name_action_image), - tapTargetView -> { - clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(), - getString(R.string.copy_field, - getString(R.string.entry_user_name))); - return null; - }, - tapTargetView -> { - // Launch autofill settings - startActivity(new Intent(EntryActivity.this, SettingsAutofillActivity.class)); - return null; - }) - ); - else if (toolbar.findViewById(R.id.menu_edit) != null - && entryActivityEducation.checkAndPerformedEntryEditEducation( - toolbar.findViewById(R.id.menu_edit), - tapTargetView -> { - onOptionsItemSelected(menu.findItem(R.id.menu_edit)); - return null; - }, - tapTargetView -> { - // Open Keepass doc to create field references - startActivity(new Intent(Intent.ACTION_VIEW, - Uri.parse(getString(R.string.field_references_url)))); - return null; - }) - ); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch ( item.getItemId() ) { - case R.id.menu_contribute: - return MenuUtil.INSTANCE.onContributionItemSelected(this); - - case R.id.menu_toggle_pass: - mShowPassword = !mShowPassword; - changeShowPasswordIcon(item); - entryContentsView.setHiddenPasswordStyle(!mShowPassword); - return true; - - case R.id.menu_edit: - EntryEditActivity.launch(EntryActivity.this, mEntry); - return true; - - case R.id.menu_goto_url: - String url; - url = mEntry.getUrl(); - - // Default http:// if no protocol specified - if ( ! url.contains("://") ) { - url = "http://" + url; - } - - try { - Util.gotoUrl(this, url); - } catch (ActivityNotFoundException e) { - Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show(); - } - return true; - - case R.id.menu_lock: - lockAndExit(); - return true; - - case android.R.id.home : - finish(); // close this activity and return to preview activity (if there is any) - } - - return super.onOptionsItemSelected(item); - } - - - @Override - public void finish() { - // Transit data in previous Activity after an update - /* - TODO Slowdown when add entry as result - Intent intent = new Intent(); - intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry); - setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent); - */ - super.finish(); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt new file mode 100644 index 000000000..afe2d1d2a --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -0,0 +1,389 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + */ +package com.kunzisoft.keepass.activities + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.graphics.Color +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.support.v7.app.AlertDialog +import android.support.v7.widget.Toolbar +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.lock.LockingHideActivity +import com.kunzisoft.keepass.app.App +import com.kunzisoft.keepass.database.element.EntryVersioned +import com.kunzisoft.keepass.database.element.PwNodeId +import com.kunzisoft.keepass.education.EntryActivityEducation +import com.kunzisoft.keepass.notifications.NotificationEntryCopyManager +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields +import com.kunzisoft.keepass.settings.SettingsAutofillActivity +import com.kunzisoft.keepass.timeout.ClipboardHelper +import com.kunzisoft.keepass.timeout.TimeoutHelper +import com.kunzisoft.keepass.utils.MenuUtil +import com.kunzisoft.keepass.utils.Util +import com.kunzisoft.keepass.view.EntryContentsView + +class EntryActivity : LockingHideActivity() { + + private var titleIconView: ImageView? = null + private var titleView: TextView? = null + private var entryContentsView: EntryContentsView? = null + private var toolbar: Toolbar? = null + + private var mEntry: EntryVersioned? = null + private var mShowPassword: Boolean = false + + private var clipboardHelper: ClipboardHelper? = null + private var firstLaunchOfActivity: Boolean = false + + private var iconColor: Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.entry_view) + + toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + + val currentDatabase = App.currentDatabase + readOnly = currentDatabase.isReadOnly || readOnly + + mShowPassword = !PreferencesUtil.isPasswordMask(this) + + // Get Entry from UUID + try { + val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY) + mEntry = currentDatabase.getEntryById(keyEntry) + } catch (e: ClassCastException) { + Log.e(TAG, "Unable to retrieve the entry key") + } + + if (mEntry == null) { + Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show() + finish() + return + } + + // Update last access time. + mEntry?.touch(modified = false, touchParents = false) + + // Retrieve the textColor to tint the icon + iconColor = theme. + obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) + .getColor(0, Color.WHITE) + + // Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set + invalidateOptionsMenu() + + // Get views + titleIconView = findViewById(R.id.entry_icon) + titleView = findViewById(R.id.entry_title) + entryContentsView = findViewById(R.id.entry_contents) + entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)) + + // Init the clipboard helper + clipboardHelper = ClipboardHelper(this) + firstLaunchOfActivity = true + } + + override fun onResume() { + super.onResume() + + mEntry?.let { entry -> + // Fill data in resume to update from EntryEditActivity + fillEntryDataInContentsView(entry) + // Refresh Menu + invalidateOptionsMenu() + // Manage entry copy to start notification if allowed + NotificationEntryCopyManager.launchNotificationIfAllowed(this, + firstLaunchOfActivity, + entry) + } + + firstLaunchOfActivity = false + } + + private fun fillEntryDataInContentsView(entry: EntryVersioned) { + + val database = App.currentDatabase + database.startManageEntry(entry) + // Assign title icon + database.drawFactory.assignDatabaseIconTo(this, titleIconView, entry.icon, iconColor) + + // Assign title text + titleView?.text = entry.getVisualTitle() + + // Assign basic fields + entryContentsView?.assignUserName(entry.username) + entryContentsView?.assignUserNameCopyListener(View.OnClickListener { + clipboardHelper?.timeoutCopyToClipboard(entry.username, + getString(R.string.copy_field, + getString(R.string.entry_user_name))) + }) + + val allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this) + entryContentsView?.assignPassword(entry.password, allowCopyPassword) + if (allowCopyPassword) { + entryContentsView?.assignPasswordCopyListener(View.OnClickListener { + clipboardHelper?.timeoutCopyToClipboard(entry.password, + getString(R.string.copy_field, + getString(R.string.entry_password))) + }) + } else { + // If dialog not already shown + if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) { + entryContentsView?.assignPasswordCopyListener(View.OnClickListener { + val message = getString(R.string.allow_copy_password_warning) + + "\n\n" + + getString(R.string.clipboard_warning) + val warningDialog = AlertDialog.Builder(this@EntryActivity) + .setMessage(message).create() + warningDialog.setButton(AlertDialog.BUTTON1, getText(android.R.string.ok) + ) { dialog, _ -> + PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true) + dialog.dismiss() + fillEntryDataInContentsView(entry) + } + warningDialog.setButton(AlertDialog.BUTTON2, getText(android.R.string.cancel) + ) { dialog, _ -> + PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false) + dialog.dismiss() + fillEntryDataInContentsView(entry) + } + warningDialog.show() + }) + } else { + entryContentsView?.assignPasswordCopyListener(null) + } + } + + entryContentsView?.assignURL(entry.url) + entryContentsView?.setHiddenPasswordStyle(!mShowPassword) + entryContentsView?.assignComment(entry.notes) + + // Assign custom fields + if (entry.allowExtraFields()) { + entryContentsView?.clearExtraFields() + + entry.fields.doActionToAllCustomProtectedField { label, value -> + val showAction = !value.isProtected || PreferencesUtil.allowCopyPasswordAndProtectedFields(this@EntryActivity) + entryContentsView?.addExtraField(label, value, showAction, View.OnClickListener { + clipboardHelper?.timeoutCopyToClipboard( + value.toString(), + getString(R.string.copy_field, label) + ) + }) + } + } + + // Assign dates + entry.creationTime.date?.let { + entryContentsView?.assignCreationDate(it) + } + entry.lastModificationTime.date?.let { + entryContentsView?.assignModificationDate(it) + } + entry.lastAccessTime.date?.let { + entryContentsView?.assignLastAccessDate(it) + } + val expires = entry.expiryTime.date + if (entry.isExpires && expires != null) { + entryContentsView?.assignExpiresDate(expires) + } else { + entryContentsView?.assignExpiresDate(getString(R.string.never)) + } + + database.stopManageEntry(entry) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> + // Not directly get the entry from intent data but from database + mEntry?.let { + fillEntryDataInContentsView(it) + } + } + } + + private fun changeShowPasswordIcon(togglePassword: MenuItem?) { + if (mShowPassword) { + togglePassword?.setTitle(R.string.menu_hide_password) + togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp) + } else { + togglePassword?.setTitle(R.string.menu_showpass) + togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + + val inflater = menuInflater + MenuUtil.contributionMenuInflater(inflater, menu) + inflater.inflate(R.menu.entry, menu) + inflater.inflate(R.menu.database_lock, menu) + + if (readOnly) { + menu.findItem(R.id.menu_edit)?.isVisible = false + } + + val togglePassword = menu.findItem(R.id.menu_toggle_pass) + entryContentsView?.let { + if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) { + changeShowPasswordIcon(togglePassword) + } else { + togglePassword?.isVisible = false + } + } + + val gotoUrl = menu.findItem(R.id.menu_goto_url) + gotoUrl?.apply { + // In API >= 11 onCreateOptionsMenu may be called before onCreate completes + // so mEntry may not be set + if (mEntry == null) { + isVisible = false + } else { + if (mEntry?.url?.isEmpty() != false) { + // disable button if url is not available + isVisible = false + } + } + } + + // Show education views + Handler().post { performedNextEducation(EntryActivityEducation(this), menu) } + + return true + } + + private fun performedNextEducation(entryActivityEducation: EntryActivityEducation, + menu: Menu) { + if (entryContentsView?.isUserNamePresent == true + && entryActivityEducation.checkAndPerformedEntryCopyEducation( + findViewById(R.id.entry_user_name_action_image), + { + clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username, + getString(R.string.copy_field, + getString(R.string.entry_user_name))) + }, + { + // Launch autofill settings + startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java)) + })) + else if (toolbar?.findViewById(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation( + toolbar!!.findViewById(R.id.menu_edit), + { + onOptionsItemSelected(menu.findItem(R.id.menu_edit)) + }, + { + // Open Keepass doc to create field references + startActivity(Intent(Intent.ACTION_VIEW, + Uri.parse(getString(R.string.field_references_url)))) + })) + ; + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this) + + R.id.menu_toggle_pass -> { + mShowPassword = !mShowPassword + changeShowPasswordIcon(item) + entryContentsView?.setHiddenPasswordStyle(!mShowPassword) + return true + } + + R.id.menu_edit -> { + EntryEditActivity.launch(this@EntryActivity, mEntry) + return true + } + + R.id.menu_goto_url -> { + var url: String = mEntry?.url ?: "" + + // Default http:// if no protocol specified + if (!url.contains("://")) { + url = "http://$url" + } + + try { + Util.gotoUrl(this, url) + } catch (e: ActivityNotFoundException) { + Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show() + } + + return true + } + + R.id.menu_lock -> { + lockAndExit() + return true + } + + android.R.id.home -> finish() // close this activity and return to preview activity (if there is any) + } + + return super.onOptionsItemSelected(item) + } + + + override fun finish() { + // Transit data in previous Activity after an update + /* + TODO Slowdown when add entry as result + Intent intent = new Intent(); + intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry); + setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent); + */ + super.finish() + } + + companion object { + private val TAG = EntryActivity::class.java.name + + const val KEY_ENTRY = "entry" + + fun launch(activity: Activity, pw: EntryVersioned, readOnly: Boolean) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { + val intent = Intent(activity, EntryActivity::class.java) + intent.putExtra(KEY_ENTRY, pw.nodeId) + ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly) + activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE) + } + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java index 3b69e8688..4a0266b63 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java @@ -503,7 +503,7 @@ public class GroupActivity extends LockingActivity EntryVersioned entry = ((EntryVersioned) node); EntrySelectionHelper.INSTANCE.doEntrySelectionAction(getIntent(), () -> { - EntryActivity.launch(GroupActivity.this, entry, getReadOnly()); + EntryActivity.Companion.launch(GroupActivity.this, entry, getReadOnly()); return null; }, () -> { diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationEntryCopyManager.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationEntryCopyManager.kt new file mode 100644 index 000000000..1573dea8d --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationEntryCopyManager.kt @@ -0,0 +1,90 @@ +package com.kunzisoft.keepass.notifications + +import android.content.Context +import android.content.Intent +import android.util.Log +import com.kunzisoft.keepass.app.App +import com.kunzisoft.keepass.database.element.EntryVersioned +import com.kunzisoft.keepass.database.element.security.ProtectedString +import com.kunzisoft.keepass.settings.PreferencesUtil +import java.util.* + + +object NotificationEntryCopyManager { + + fun launchNotificationIfAllowed(context: Context, firstLaunch: Boolean, entry: EntryVersioned) { + // Start to manage field reference to copy a value from ref + val database = App.currentDatabase + database.startManageEntry(entry) + + val containsUsernameToCopy = entry.username.isNotEmpty() + val containsPasswordToCopy = entry.password.isNotEmpty() + && PreferencesUtil.allowCopyPasswordAndProtectedFields(context) + val containsExtraFieldToCopy = entry.allowExtraFields() + && (entry.containsCustomFields() + && entry.containsCustomFieldsNotProtected() + || (entry.containsCustomFields() + && entry.containsCustomFieldsProtected() + && PreferencesUtil.allowCopyPasswordAndProtectedFields(context)) + ) + + // If notifications enabled in settings + // Don't if application timeout + if (firstLaunch + && PreferencesUtil.isClipboardNotificationsEnable(context.applicationContext)) { + if (containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) { + + // username already copied, waiting for user's action before copy password. + val intent = Intent(context, NotificationCopyingService::class.java) + intent.action = NotificationCopyingService.ACTION_NEW_NOTIFICATION + intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE, entry.title) + // Construct notification fields + val notificationFields = ArrayList() + // Add username if exists to notifications + if (containsUsernameToCopy) + notificationFields.add( + NotificationField( + NotificationField.NotificationFieldId.USERNAME, + entry.username, + context.resources)) + // Add password to notifications + if (containsPasswordToCopy) { + notificationFields.add( + NotificationField( + NotificationField.NotificationFieldId.PASSWORD, + entry.password, + context.resources)) + } + // Add extra fields + if (containsExtraFieldToCopy) { + try { + entry.fields.doActionToAllCustomProtectedField(object : (String, ProtectedString) -> Unit { + private var anonymousFieldNumber = 0 + override fun invoke(key: String, value: ProtectedString) { + //If value is not protected or allowed + if (!value.isProtected + || PreferencesUtil.allowCopyPasswordAndProtectedFields(context)) { + notificationFields.add( + NotificationField( + NotificationField.NotificationFieldId.getAnonymousFieldId()[anonymousFieldNumber], + value.toString(), + key, + context.resources)) + anonymousFieldNumber++ + } + } + }) + } catch (e: ArrayIndexOutOfBoundsException) { + Log.w("NotificationEntryCopyMg", "Only " + NotificationField.NotificationFieldId.getAnonymousFieldId().size + + " anonymous notifications are available") + } + + } + // Add notifications + intent.putParcelableArrayListExtra(NotificationCopyingService.EXTRA_FIELDS, notificationFields) + context.startService(intent) + } + } + database.stopManageEntry(entry) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.java b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.java deleted file mode 100644 index 9eeea705d..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.java +++ /dev/null @@ -1,253 +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.view; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.support.v4.content.ContextCompat; -import android.text.method.PasswordTransformationMethod; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.database.element.security.ProtectedString; -import com.kunzisoft.keepass.utils.Util; - -import java.text.DateFormat; -import java.util.Date; - -public class EntryContentsView extends LinearLayout { - - private boolean fontInVisibility; - private int colorAccent; - - private View userNameContainerView; - private TextView userNameView; - private ImageView userNameActionView; - - private View passwordContainerView; - private TextView passwordView; - private ImageView passwordActionView; - - private View urlContainerView; - private TextView urlView; - - private View commentContainerView; - private TextView commentView; - - private ViewGroup extrasView; - - private DateFormat dateFormat; - private DateFormat timeFormat; - - private TextView creationDateView; - private TextView modificationDateView; - private TextView lastAccessDateView; - private TextView expiresDateView; - - public EntryContentsView(Context context) { - this(context, null); - } - - public EntryContentsView(Context context, AttributeSet attrs) { - super(context, attrs); - - fontInVisibility = false; - - dateFormat = android.text.format.DateFormat.getDateFormat(context); - timeFormat = android.text.format.DateFormat.getTimeFormat(context); - - inflate(context); - - int[] attrColorAccent = {R.attr.colorAccentCompat}; - TypedArray taColorAccent = context.getTheme().obtainStyledAttributes(attrColorAccent); - this.colorAccent = taColorAccent.getColor(0, Color.BLACK); - taColorAccent.recycle(); - } - - private void inflate(Context context) { - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - assert inflater != null; - inflater.inflate(R.layout.entry_view_contents, this); - - userNameContainerView = findViewById(R.id.entry_user_name_container); - userNameView = findViewById(R.id.entry_user_name); - userNameActionView = findViewById(R.id.entry_user_name_action_image); - - passwordContainerView = findViewById(R.id.entry_password_container); - passwordView = findViewById(R.id.entry_password); - passwordActionView = findViewById(R.id.entry_password_action_image); - - urlContainerView = findViewById(R.id.entry_url_container); - urlView = findViewById(R.id.entry_url); - - commentContainerView = findViewById(R.id.entry_notes_container); - commentView = findViewById(R.id.entry_notes); - - extrasView = findViewById(R.id.extra_strings); - - creationDateView = findViewById(R.id.entry_created); - modificationDateView = findViewById(R.id.entry_modified); - lastAccessDateView = findViewById(R.id.entry_accessed); - expiresDateView = findViewById(R.id.entry_expires); - } - - public void applyFontVisibilityToFields(boolean fontInVisibility) { - this.fontInVisibility = fontInVisibility; - } - - public void assignUserName(String userName) { - if (userName != null && !userName.isEmpty()) { - userNameContainerView.setVisibility(VISIBLE); - userNameView.setText(userName); - if (fontInVisibility) - Util.applyFontVisibilityTo(getContext(), userNameView); - } else { - userNameContainerView.setVisibility(GONE); - } - } - - public void assignUserNameCopyListener(OnClickListener onClickListener) { - userNameActionView.setOnClickListener(onClickListener); - } - - public boolean isUserNamePresent() { - return userNameContainerView.getVisibility() == VISIBLE; - } - - public void assignPassword(String password, boolean allowCopyPassword) { - if (password != null && !password.isEmpty()) { - passwordContainerView.setVisibility(VISIBLE); - passwordView.setText(password); - if (fontInVisibility) - Util.applyFontVisibilityTo(getContext(), passwordView); - if (!allowCopyPassword) { - passwordActionView.setColorFilter(ContextCompat.getColor(getContext(), R.color.grey_dark)); - } else { - passwordActionView.setColorFilter(colorAccent); - } - } else { - passwordContainerView.setVisibility(GONE); - } - } - - public void assignPasswordCopyListener(OnClickListener onClickListener) { - if (onClickListener == null) - setClickable(false); - passwordActionView.setOnClickListener(onClickListener); - } - - public boolean isPasswordPresent() { - return passwordContainerView.getVisibility() == VISIBLE; - } - - public boolean atLeastOneFieldProtectedPresent() { - for (int i = 0; i < extrasView.getChildCount(); i++) { - View childCustomView = extrasView.getChildAt(i); - if (childCustomView instanceof EntryCustomFieldProtected) - return true; - } - return false; - } - - public void setHiddenPasswordStyle(boolean hiddenStyle) { - if ( !hiddenStyle ) { - passwordView.setTransformationMethod(null); - } else { - passwordView.setTransformationMethod(PasswordTransformationMethod.getInstance()); - } - // Hidden style for custom fields - for (int i = 0; i < extrasView.getChildCount(); i++) { - View childCustomView = extrasView.getChildAt(i); - if (childCustomView instanceof EntryCustomFieldProtected) - ((EntryCustomFieldProtected) childCustomView).setHiddenPasswordStyle(hiddenStyle); - } - } - - public void assignURL(String url) { - if (url != null && !url.isEmpty()) { - urlContainerView.setVisibility(VISIBLE); - urlView.setText(url); - } else { - urlContainerView.setVisibility(GONE); - } - } - - public void assignComment(String comment) { - if (comment != null && !comment.isEmpty()) { - commentContainerView.setVisibility(VISIBLE); - commentView.setText(comment); - if (fontInVisibility) - Util.applyFontVisibilityTo(getContext(), commentView); - } else { - commentContainerView.setVisibility(GONE); - } - } - - public void addExtraField(String title, ProtectedString value, boolean showAction, OnClickListener onActionClickListener) { - EntryCustomField entryCustomField; - if (value.isProtected()) - entryCustomField = new EntryCustomFieldProtected(getContext(), null, title, value, showAction, onActionClickListener); - else - entryCustomField = new EntryCustomField(getContext(), null, title, value, showAction, onActionClickListener); - entryCustomField.applyFontVisibility(fontInVisibility); - extrasView.addView(entryCustomField); - } - - public void clearExtraFields() { - extrasView.removeAllViews(); - } - - private String getDateTime(Date date) { - return dateFormat.format(date) + " " + timeFormat.format(date); - } - - public void assignCreationDate(Date date) { - creationDateView.setText(getDateTime(date)); - } - - public void assignModificationDate(Date date) { - modificationDateView.setText(getDateTime(date)); - } - - public void assignLastAccessDate(Date date) { - lastAccessDateView.setText(getDateTime(date)); - } - - public void assignExpiresDate(Date date) { - expiresDateView.setText(getDateTime(date)); - } - - public void assignExpiresDate(String constString) { - expiresDateView.setText(constString); - } - - @Override - protected LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - } - -} diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt new file mode 100644 index 000000000..cc38c8bcf --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt @@ -0,0 +1,250 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + */ +package com.kunzisoft.keepass.view + +import android.content.Context +import android.graphics.Color +import android.support.v4.content.ContextCompat +import android.text.method.PasswordTransformationMethod +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.security.ProtectedString +import com.kunzisoft.keepass.utils.Util +import java.text.DateFormat +import java.util.* + +class EntryContentsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) { + + private var fontInVisibility: Boolean = false + private val colorAccent: Int + + private var userNameContainerView: View? = null + private var userNameView: TextView? = null + private var userNameActionView: ImageView? = null + + private var passwordContainerView: View? = null + private var passwordView: TextView? = null + private var passwordActionView: ImageView? = null + + private var urlContainerView: View? = null + private var urlView: TextView? = null + + private var commentContainerView: View? = null + private var commentView: TextView? = null + + private var extrasView: ViewGroup? = null + + private val dateFormat: DateFormat + private val timeFormat: DateFormat + + private var creationDateView: TextView? = null + private var modificationDateView: TextView? = null + private var lastAccessDateView: TextView? = null + private var expiresDateView: TextView? = null + + val isUserNamePresent: Boolean + get() = userNameContainerView!!.visibility == View.VISIBLE + + val isPasswordPresent: Boolean + get() = passwordContainerView!!.visibility == View.VISIBLE + + init { + + fontInVisibility = false + + dateFormat = android.text.format.DateFormat.getDateFormat(context) + timeFormat = android.text.format.DateFormat.getTimeFormat(context) + + inflate(context) + + val attrColorAccent = intArrayOf(R.attr.colorAccentCompat) + val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent) + this.colorAccent = taColorAccent.getColor(0, Color.BLACK) + taColorAccent.recycle() + } + + private fun inflate(context: Context) { + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + inflater.inflate(R.layout.entry_view_contents, this) + + userNameContainerView = findViewById(R.id.entry_user_name_container) + userNameView = findViewById(R.id.entry_user_name) + userNameActionView = findViewById(R.id.entry_user_name_action_image) + + passwordContainerView = findViewById(R.id.entry_password_container) + passwordView = findViewById(R.id.entry_password) + passwordActionView = findViewById(R.id.entry_password_action_image) + + urlContainerView = findViewById(R.id.entry_url_container) + urlView = findViewById(R.id.entry_url) + + commentContainerView = findViewById(R.id.entry_notes_container) + commentView = findViewById(R.id.entry_notes) + + extrasView = findViewById(R.id.extra_strings) + + creationDateView = findViewById(R.id.entry_created) + modificationDateView = findViewById(R.id.entry_modified) + lastAccessDateView = findViewById(R.id.entry_accessed) + expiresDateView = findViewById(R.id.entry_expires) + } + + fun applyFontVisibilityToFields(fontInVisibility: Boolean) { + this.fontInVisibility = fontInVisibility + } + + fun assignUserName(userName: String?) { + if (userName != null && userName.isNotEmpty()) { + userNameContainerView?.visibility = View.VISIBLE + userNameView?.apply { + text = userName + if (fontInVisibility) + Util.applyFontVisibilityTo(context, this) + } + } else { + userNameContainerView?.visibility = View.GONE + } + } + + fun assignUserNameCopyListener(onClickListener: OnClickListener) { + userNameActionView?.setOnClickListener(onClickListener) + } + + fun assignPassword(password: String?, allowCopyPassword: Boolean) { + if (password != null && password.isNotEmpty()) { + passwordContainerView?.visibility = View.VISIBLE + passwordView?.apply { + text = password + if (fontInVisibility) + Util.applyFontVisibilityTo(context, this) + } + if (!allowCopyPassword) { + passwordActionView?.setColorFilter(ContextCompat.getColor(context, R.color.grey_dark)) + } else { + passwordActionView?.setColorFilter(colorAccent) + } + } else { + passwordContainerView?.visibility = View.GONE + } + } + + fun assignPasswordCopyListener(onClickListener: OnClickListener?) { + if (onClickListener == null) + isClickable = false + passwordActionView?.setOnClickListener(onClickListener) + } + + fun atLeastOneFieldProtectedPresent(): Boolean { + extrasView?.let { + for (i in 0 until it.childCount) { + val childCustomView = it.getChildAt(i) + if (childCustomView is EntryCustomFieldProtected) + return true + } + } + return false + } + + fun setHiddenPasswordStyle(hiddenStyle: Boolean) { + if (!hiddenStyle) { + passwordView?.transformationMethod = null + } else { + passwordView?.transformationMethod = PasswordTransformationMethod.getInstance() + } + // Hidden style for custom fields + extrasView?.let { + for (i in 0 until it.childCount) { + val childCustomView = it.getChildAt(i) + if (childCustomView is EntryCustomFieldProtected) + childCustomView.setHiddenPasswordStyle(hiddenStyle) + } + } + } + + fun assignURL(url: String?) { + if (url != null && url.isNotEmpty()) { + urlContainerView?.visibility = View.VISIBLE + urlView?.text = url + } else { + urlContainerView?.visibility = View.GONE + } + } + + fun assignComment(comment: String?) { + if (comment != null && comment.isNotEmpty()) { + commentContainerView?.visibility = View.VISIBLE + commentView?.apply { + text = comment + if (fontInVisibility) + Util.applyFontVisibilityTo(context, this) + } + + } else { + commentContainerView?.visibility = View.GONE + } + } + + fun addExtraField(title: String, value: ProtectedString, showAction: Boolean, onActionClickListener: OnClickListener) { + val entryCustomField: EntryCustomField + if (value.isProtected) + entryCustomField = EntryCustomFieldProtected(context, null, title, value, showAction, onActionClickListener) + else + entryCustomField = EntryCustomField(context, null, title, value, showAction, onActionClickListener) + entryCustomField.applyFontVisibility(fontInVisibility) + extrasView?.addView(entryCustomField) + } + + fun clearExtraFields() { + extrasView?.removeAllViews() + } + + private fun getDateTime(date: Date): String { + return dateFormat.format(date) + " " + timeFormat.format(date) + } + + fun assignCreationDate(date: Date) { + creationDateView?.text = getDateTime(date) + } + + fun assignModificationDate(date: Date) { + modificationDateView?.text = getDateTime(date) + } + + fun assignLastAccessDate(date: Date) { + lastAccessDateView?.text = getDateTime(date) + } + + fun assignExpiresDate(date: Date) { + expiresDateView?.text = getDateTime(date) + } + + fun assignExpiresDate(constString: String) { + expiresDateView?.text = constString + } + + override fun generateDefaultLayoutParams(): LayoutParams { + return LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + } +} From cc35a1e8aac35bbff4c21168e25f9e6918941d6e Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 7 Jul 2019 18:47:32 +0200 Subject: [PATCH 07/24] Fix deprecated buttons --- .../com/kunzisoft/keepass/activities/EntryActivity.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 afe2d1d2a..04f346e0b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -169,13 +169,13 @@ class EntryActivity : LockingHideActivity() { getString(R.string.clipboard_warning) val warningDialog = AlertDialog.Builder(this@EntryActivity) .setMessage(message).create() - warningDialog.setButton(AlertDialog.BUTTON1, getText(android.R.string.ok) + warningDialog.setButton(AlertDialog.BUTTON_POSITIVE, getText(android.R.string.ok) ) { dialog, _ -> PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true) dialog.dismiss() fillEntryDataInContentsView(entry) } - warningDialog.setButton(AlertDialog.BUTTON2, getText(android.R.string.cancel) + warningDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getText(android.R.string.cancel) ) { dialog, _ -> PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false) dialog.dismiss() @@ -328,7 +328,9 @@ class EntryActivity : LockingHideActivity() { } R.id.menu_edit -> { - EntryEditActivity.launch(this@EntryActivity, mEntry) + mEntry?.let { + EntryEditActivity.launch(this@EntryActivity, it) + } return true } From 191e0cc654a26e36023cda525e13c9531c85e209 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 7 Jul 2019 18:48:56 +0200 Subject: [PATCH 08/24] Kotlinized EntryEditActivity --- .../keepass/activities/EntryEditActivity.java | 567 ------------------ .../keepass/activities/EntryEditActivity.kt | 542 +++++++++++++++++ .../keepass/activities/GroupActivity.java | 4 +- .../activities/lock/LockingActivity.kt | 4 +- 4 files changed, 546 insertions(+), 571 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.java deleted file mode 100644 index 5ee760dcb..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.java +++ /dev/null @@ -1,567 +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.activities; - -import android.app.Activity; -import android.content.Intent; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.os.Bundle; -import android.os.Handler; -import android.support.v7.widget.Toolbar; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ScrollView; -import android.widget.Toast; - -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.activities.lock.LockingHideActivity; -import com.kunzisoft.keepass.app.App; -import com.kunzisoft.keepass.database.action.node.ActionNodeValues; -import com.kunzisoft.keepass.database.action.node.AddEntryRunnable; -import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable; -import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable; -import com.kunzisoft.keepass.database.element.Database; -import com.kunzisoft.keepass.database.element.EntryVersioned; -import com.kunzisoft.keepass.database.element.GroupVersioned; -import com.kunzisoft.keepass.database.element.PwDate; -import com.kunzisoft.keepass.database.element.PwIcon; -import com.kunzisoft.keepass.database.element.PwIconStandard; -import com.kunzisoft.keepass.database.element.PwNodeId; -import com.kunzisoft.keepass.database.element.security.ProtectedString; -import com.kunzisoft.keepass.dialogs.GeneratePasswordDialogFragment; -import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment; -import com.kunzisoft.keepass.education.EntryEditActivityEducation; -import com.kunzisoft.keepass.settings.PreferencesUtil; -import com.kunzisoft.keepass.tasks.ActionRunnable; -import com.kunzisoft.keepass.timeout.TimeoutHelper; -import com.kunzisoft.keepass.utils.MenuUtil; -import com.kunzisoft.keepass.utils.Util; -import com.kunzisoft.keepass.view.EntryEditCustomField; - -import org.jetbrains.annotations.NotNull; - -import static com.kunzisoft.keepass.dialogs.IconPickerDialogFragment.KEY_ICON_STANDARD; - -public class EntryEditActivity extends LockingHideActivity - implements IconPickerDialogFragment.IconPickerListener, - GeneratePasswordDialogFragment.GeneratePasswordListener { - - private static final String TAG = EntryEditActivity.class.getName(); - - // Keys for current Activity - public static final String KEY_ENTRY = "entry"; - public static final String KEY_PARENT = "parent"; - - // Keys for callback - public static final int ADD_ENTRY_RESULT_CODE = 31; - public static final int UPDATE_ENTRY_RESULT_CODE = 32; - public static final int ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129; - public static final String ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"; - - private Database database; - - protected EntryVersioned mEntry; - protected GroupVersioned mParent; - protected EntryVersioned mNewEntry; - protected boolean mIsNew; - protected PwIconStandard mSelectedIconStandard; - - // Views - private ScrollView scrollView; - private EditText entryTitleView; - private ImageView entryIconView; - private EditText entryUserNameView; - private EditText entryUrlView; - private EditText entryPasswordView; - private EditText entryConfirmationPasswordView; - private View generatePasswordView; - private EditText entryCommentView; - private ViewGroup entryExtraFieldsContainer; - private View addNewFieldView; - private View saveView; - private int iconColor; - - // Education - private EntryEditActivityEducation entryEditActivityEducation; - - /** - * Launch EntryEditActivity to update an existing entry - * - * @param activity from activity - * @param pwEntry Entry to update - */ - public static void launch(Activity activity, EntryVersioned pwEntry) { - if (TimeoutHelper.INSTANCE.checkTimeAndLockIfTimeout(activity)) { - Intent intent = new Intent(activity, EntryEditActivity.class); - intent.putExtra(KEY_ENTRY, pwEntry.getNodeId()); - activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE); - } - } - - /** - * Launch EntryEditActivity to add a new entry - * - * @param activity from activity - * @param pwGroup Group who will contains new entry - */ - public static void launch(Activity activity, GroupVersioned pwGroup) { - if (TimeoutHelper.INSTANCE.checkTimeAndLockIfTimeout(activity)) { - Intent intent = new Intent(activity, EntryEditActivity.class); - intent.putExtra(KEY_PARENT, pwGroup.getNodeId()); - activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.entry_edit); - - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - assert getSupportActionBar() != null; - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setDisplayShowHomeEnabled(true); - - scrollView = findViewById(R.id.entry_edit_scroll); - scrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); - - entryTitleView = findViewById(R.id.entry_edit_title); - entryIconView = findViewById(R.id.entry_edit_icon_button); - entryUserNameView = findViewById(R.id.entry_edit_user_name); - entryUrlView = findViewById(R.id.entry_edit_url); - entryPasswordView = findViewById(R.id.entry_edit_password); - entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password); - entryCommentView = findViewById(R.id.entry_edit_notes); - entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container); - - // Focus view to reinitialize timeout - resetAppTimeoutWhenViewFocusedOrChanged( - entryTitleView, - entryIconView, - entryUserNameView, - entryUrlView, - entryPasswordView, - entryConfirmationPasswordView, - entryCommentView, - entryExtraFieldsContainer); - - // Likely the app has been killed exit the activity - database = App.Companion.getCurrentDatabase(); - - // Retrieve the textColor to tint the icon - int[] attrs = {android.R.attr.textColorPrimary}; - TypedArray ta = getTheme().obtainStyledAttributes(attrs); - iconColor = ta.getColor(0, Color.WHITE); - - mSelectedIconStandard = database.getIconFactory().getUnknownIcon(); - - Intent intent = getIntent(); - // Entry is retrieve, it's an entry to update - PwNodeId keyEntry = intent.getParcelableExtra(KEY_ENTRY); - if (keyEntry != null) { - mIsNew = false; - mEntry = database.getEntryById(keyEntry); - if (mEntry != null) { - mParent = mEntry.getParent(); - fillData(); - } - } - - // Parent is retrieve, it's a new entry to create - PwNodeId keyParent = intent.getParcelableExtra(KEY_PARENT); - if (keyParent != null) { - mIsNew = true; - mEntry = database.createEntry(); - mParent = database.getGroupById(keyParent); - // Add the default icon - database.getDrawFactory().assignDefaultDatabaseIconTo(this, entryIconView, iconColor); - } - - // Close the activity if entry or parent can't be retrieve - if (mEntry == null || mParent == null) { - finish(); - return; - } - - // Assign title - setTitle((mIsNew) ? getString(R.string.add_entry) : getString(R.string.edit_entry)); - - // Retrieve the icon after an orientation change - if (savedInstanceState != null - && savedInstanceState.containsKey(KEY_ICON_STANDARD)) { - iconPicked(savedInstanceState); - } - - // Add listener to the icon - entryIconView.setOnClickListener(v -> - IconPickerDialogFragment.launch(EntryEditActivity.this)); - - // Generate password button - generatePasswordView = findViewById(R.id.entry_edit_generate_button); - generatePasswordView.setOnClickListener(v -> openPasswordGenerator()); - - // Save button - saveView = findViewById(R.id.entry_edit_save); - saveView.setOnClickListener(v -> saveEntry()); - - if (mEntry.allowExtraFields()) { - addNewFieldView = findViewById(R.id.entry_edit_add_new_field); - addNewFieldView.setVisibility(View.VISIBLE); - addNewFieldView.setOnClickListener(v -> addNewCustomField()); - } - - // Verify the education views - entryEditActivityEducation = new EntryEditActivityEducation(this); - new Handler().post(() -> performedNextEducation(entryEditActivityEducation)); - } - - private void performedNextEducation(EntryEditActivityEducation entryEditActivityEducation) { - if (entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( - generatePasswordView, - tapTargetView -> { - openPasswordGenerator(); - return null; - }, - tapTargetView -> { - performedNextEducation(entryEditActivityEducation); - return null; - } - )); - else if (mEntry.allowExtraFields() - && !mEntry.containsCustomFields() - && entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( - addNewFieldView, - tapTargetView -> { - addNewCustomField(); - return null; - }, - tapTargetView -> null) - ); - } - - /** - * Open the password generator fragment - */ - private void openPasswordGenerator() { - GeneratePasswordDialogFragment generatePasswordDialogFragment = new GeneratePasswordDialogFragment(); - generatePasswordDialogFragment.show(getSupportFragmentManager(), "PasswordGeneratorFragment"); - } - - /** - * Add a new view to fill in the information of the customized field - */ - private void addNewCustomField() { - EntryEditCustomField entryEditCustomField = new EntryEditCustomField(EntryEditActivity.this); - entryEditCustomField.setData("", new ProtectedString(false, "")); - boolean visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this); - entryEditCustomField.setFontVisibility(visibilityFontActivated); - entryExtraFieldsContainer.addView(entryEditCustomField); - - // Scroll bottom - scrollView.post(() -> scrollView.fullScroll(ScrollView.FOCUS_DOWN)); - } - - /** - * Saves the new entry or update an existing entry in the database - */ - private void saveEntry() { - if (!validateBeforeSaving()) { - return; - } - // Clone the entry - mNewEntry = new EntryVersioned(mEntry); - - populateEntryWithViewInfo(mNewEntry); - - // Open a progress dialog and save entry - ActionRunnable task; - AfterActionNodeFinishRunnable afterActionNodeFinishRunnable = - new AfterActionNodeFinishRunnable() { - @Override - public void onActionNodeFinish(@NotNull ActionNodeValues actionNodeValues) { - if (actionNodeValues.getSuccess()) - finish(); - } - }; - if ( mIsNew ) { - task = new AddEntryRunnable(EntryEditActivity.this, - database, - mNewEntry, - mParent, - afterActionNodeFinishRunnable, - !getReadOnly()); - } else { - task = new UpdateEntryRunnable(EntryEditActivity.this, - database, - mEntry, - mNewEntry, - afterActionNodeFinishRunnable, - !getReadOnly()); - } - new Thread(task).start(); - } - - - - /** - * Utility class to retrieve a validation or an error with a message - */ - private class ErrorValidation { - static final int unknownMessage = -1; - - boolean isValidate = false; - int messageId = unknownMessage; - - void showValidationErrorIfNeeded() { - if (!isValidate && messageId != unknownMessage) - Toast.makeText(EntryEditActivity.this, messageId, Toast.LENGTH_LONG).show(); - } - } - - /** - * Validate or not the entry form - * - * @return ErrorValidation An error with a message or a validation without message - */ - protected ErrorValidation validate() { - ErrorValidation errorValidation = new ErrorValidation(); - - // Require title - String title = entryTitleView.getText().toString(); - if ( title.length() == 0 ) { - errorValidation.messageId = R.string.error_title_required; - return errorValidation; - } - - // Validate password - String pass = entryPasswordView.getText().toString(); - String conf = entryConfirmationPasswordView.getText().toString(); - if ( ! pass.equals(conf) ) { - errorValidation.messageId = R.string.error_pass_match; - return errorValidation; - } - - // Validate extra fields - if (mEntry.allowExtraFields()) { - for (int i = 0; i < entryExtraFieldsContainer.getChildCount(); i++) { - EntryEditCustomField entryEditCustomField = (EntryEditCustomField) entryExtraFieldsContainer.getChildAt(i); - String key = entryEditCustomField.getLabel(); - if (key == null || key.length() == 0) { - errorValidation.messageId = R.string.error_string_key; - return errorValidation; - } - } - } - - errorValidation.isValidate = true; - return errorValidation; - } - - /** - * Launch a validation with {@link #validate()} and show the error if present - * - * @return true if the form was validate or false if not - */ - protected boolean validateBeforeSaving() { - ErrorValidation errorValidation = validate(); - errorValidation.showValidationErrorIfNeeded(); - return errorValidation.isValidate; - } - - private void populateEntryWithViewInfo(EntryVersioned newEntry) { - - database.startManageEntry(newEntry); - - newEntry.setLastAccessTime(new PwDate()); - newEntry.setLastModificationTime(new PwDate()); - - newEntry.setTitle(entryTitleView.getText().toString()); - newEntry.setIcon(retrieveIcon()); - - newEntry.setUrl(entryUrlView.getText().toString()); - newEntry.setUsername(entryUserNameView.getText().toString()); - newEntry.setNotes(entryCommentView.getText().toString()); - newEntry.setPassword(entryPasswordView.getText().toString()); - - if (newEntry.allowExtraFields()) { - // Delete all extra strings - newEntry.removeAllCustomFields(); - // Add extra fields from views - for (int i = 0; i < entryExtraFieldsContainer.getChildCount(); i++) { - EntryEditCustomField view = (EntryEditCustomField) entryExtraFieldsContainer.getChildAt(i); - String key = view.getLabel(); - String value = view.getValue(); - boolean protect = view.isProtected(); - newEntry.addExtraField(key, new ProtectedString(protect, value)); - } - } - - database.stopManageEntry(newEntry); - } - - /** - * Retrieve the icon by the selection, or the first icon in the list if the entry is new or the last one - */ - private PwIcon retrieveIcon() { - - if (!mSelectedIconStandard.isUnknown()) - return mSelectedIconStandard; - else { - if (mIsNew) { - return database.getIconFactory().getKeyIcon(); - } - else { - // Keep previous icon, if no new one was selected - return mEntry.getIcon(); - } - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.database_lock, menu); - MenuUtil.INSTANCE.contributionMenuInflater(inflater, menu); - - return true; - } - - public boolean onOptionsItemSelected(MenuItem item) { - switch ( item.getItemId() ) { - case R.id.menu_lock: - lockAndExit(); - return true; - - case R.id.menu_contribute: - return MenuUtil.INSTANCE.onContributionItemSelected(this); - - case android.R.id.home: - finish(); - } - - return super.onOptionsItemSelected(item); - } - - private void assignIconView() { - database.getDrawFactory() - .assignDatabaseIconTo( - this, - entryIconView, - mEntry.getIcon(), - iconColor); - } - - protected void fillData() { - - assignIconView(); - - // Don't start the field reference manager, we want to see the raw ref - App.Companion.getCurrentDatabase().stopManageEntry(mEntry); - - entryTitleView.setText(mEntry.getTitle()); - entryUserNameView.setText(mEntry.getUsername()); - entryUrlView.setText(mEntry.getUrl()); - String password = mEntry.getPassword(); - entryPasswordView.setText(password); - entryConfirmationPasswordView.setText(password); - entryCommentView.setText(mEntry.getNotes()); - - boolean visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this); - if (visibilityFontActivated) { - Util.applyFontVisibilityTo(this, entryUserNameView); - Util.applyFontVisibilityTo(this, entryPasswordView); - Util.applyFontVisibilityTo(this, entryConfirmationPasswordView); - Util.applyFontVisibilityTo(this, entryCommentView); - } - - if (mEntry.allowExtraFields()) { - LinearLayout container = findViewById(R.id.entry_edit_advanced_container); - mEntry.getFields().doActionToAllCustomProtectedField((key, value) -> { - EntryEditCustomField entryEditCustomField = new EntryEditCustomField(EntryEditActivity.this); - entryEditCustomField.setData(key, value); - entryEditCustomField.setFontVisibility(visibilityFontActivated); - container.addView(entryEditCustomField); - return null; - }); - } - } - - @Override - public void iconPicked(Bundle bundle) { - mSelectedIconStandard = bundle.getParcelable(KEY_ICON_STANDARD); - mEntry.setIcon(mSelectedIconStandard); - assignIconView(); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - if (!mSelectedIconStandard.isUnknown()) { - outState.putParcelable(KEY_ICON_STANDARD, mSelectedIconStandard); - super.onSaveInstanceState(outState); - } - } - - @Override - public void acceptPassword(Bundle bundle) { - String generatedPassword = bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID); - entryPasswordView.setText(generatedPassword); - entryConfirmationPasswordView.setText(generatedPassword); - - new Handler().post(() -> performedNextEducation(entryEditActivityEducation)); - } - - @Override - public void cancelPassword(Bundle bundle) { - // Do nothing here - } - - @Override - public void finish() { - // Assign entry callback as a result in all case - try { - if (mNewEntry != null) { - Bundle bundle = new Bundle(); - Intent intentEntry = new Intent(); - bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry); - intentEntry.putExtras(bundle); - if (mIsNew) { - setResult(ADD_ENTRY_RESULT_CODE, intentEntry); - } else { - setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry); - } - } - super.finish(); - } catch (Exception e) { - // Exception when parcelable can't be done - Log.e(TAG, "Cant add entry as result", e); - } - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt new file mode 100644 index 000000000..4797af08b --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -0,0 +1,542 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + */ +package com.kunzisoft.keepass.activities + +import android.app.Activity +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.os.Handler +import android.support.v7.widget.Toolbar +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.Toast + +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.lock.LockingHideActivity +import com.kunzisoft.keepass.app.App +import com.kunzisoft.keepass.database.action.node.ActionNodeValues +import com.kunzisoft.keepass.database.action.node.AddEntryRunnable +import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable +import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.EntryVersioned +import com.kunzisoft.keepass.database.element.GroupVersioned +import com.kunzisoft.keepass.database.element.PwDate +import com.kunzisoft.keepass.database.element.PwIcon +import com.kunzisoft.keepass.database.element.PwIconStandard +import com.kunzisoft.keepass.database.element.PwNodeId +import com.kunzisoft.keepass.database.element.security.ProtectedString +import com.kunzisoft.keepass.dialogs.GeneratePasswordDialogFragment +import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment +import com.kunzisoft.keepass.education.EntryEditActivityEducation +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.timeout.TimeoutHelper +import com.kunzisoft.keepass.utils.MenuUtil +import com.kunzisoft.keepass.utils.Util +import com.kunzisoft.keepass.view.EntryEditCustomField + +import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment.KEY_ICON_STANDARD + +class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener { + + private var mDatabase: Database? = null + + private var mEntry: EntryVersioned? = null + private var mParent: GroupVersioned? = null + private var mNewEntry: EntryVersioned? = null + private var mIsNew: Boolean = false + private var mSelectedIconStandard: PwIconStandard? = null + + // Views + private var scrollView: ScrollView? = null + private var entryTitleView: EditText? = null + private var entryIconView: ImageView? = null + private var entryUserNameView: EditText? = null + private var entryUrlView: EditText? = null + private var entryPasswordView: EditText? = null + private var entryConfirmationPasswordView: EditText? = null + private var generatePasswordView: View? = null + private var entryCommentView: EditText? = null + private var entryExtraFieldsContainer: ViewGroup? = null + private var addNewFieldView: View? = null + private var saveView: View? = null + private var iconColor: Int = 0 + + // View validation message + private var validationErrorMessageId = UNKNOWN_MESSAGE + + // Education + private var entryEditActivityEducation: EntryEditActivityEducation? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.entry_edit) + + val toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + + scrollView = findViewById(R.id.entry_edit_scroll) + scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET + + entryTitleView = findViewById(R.id.entry_edit_title) + entryIconView = findViewById(R.id.entry_edit_icon_button) + entryUserNameView = findViewById(R.id.entry_edit_user_name) + entryUrlView = findViewById(R.id.entry_edit_url) + entryPasswordView = findViewById(R.id.entry_edit_password) + entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password) + entryCommentView = findViewById(R.id.entry_edit_notes) + entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container) + + // Focus view to reinitialize timeout + resetAppTimeoutWhenViewFocusedOrChanged( + entryTitleView, + entryIconView, + entryUserNameView, + entryUrlView, + entryPasswordView, + entryConfirmationPasswordView, + entryCommentView, + entryExtraFieldsContainer) + + // Likely the app has been killed exit the activity + mDatabase = App.currentDatabase + + // Retrieve the textColor to tint the icon + iconColor = theme + .obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) + .getColor(0, Color.WHITE) + + mSelectedIconStandard = mDatabase?.iconFactory?.unknownIcon + + // Entry is retrieve, it's an entry to update + intent.getParcelableExtra>(KEY_ENTRY)?.let { + mIsNew = false + mEntry = mDatabase?.getEntryById(it) + mEntry?.let { entry -> + mParent = entry.parent + fillEntryDataInContentsView(entry) + } + } + + // Parent is retrieve, it's a new entry to create + intent.getParcelableExtra>(KEY_PARENT)?.let { + mIsNew = true + mEntry = mDatabase?.createEntry() + mParent = mDatabase?.getGroupById(it) + // Add the default icon + mDatabase?.drawFactory?.assignDefaultDatabaseIconTo(this, entryIconView, iconColor) + } + + // Close the activity if entry or parent can't be retrieve + if (mEntry == null || mParent == null) { + finish() + return + } + + // Assign title + title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry) + + // Retrieve the icon after an orientation change + savedInstanceState?.let { + if (it.containsKey(KEY_ICON_STANDARD)) { + iconPicked(it) + } + } + + // Add listener to the icon + entryIconView?.setOnClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) } + + // Generate password button + generatePasswordView = findViewById(R.id.entry_edit_generate_button) + generatePasswordView?.setOnClickListener { openPasswordGenerator() } + + // Save button + saveView = findViewById(R.id.entry_edit_save) + mEntry?.let { entry -> + saveView?.setOnClickListener { saveEntry(entry) } + } + + if (mEntry?.allowExtraFields() == true) { + addNewFieldView = findViewById(R.id.entry_edit_add_new_field) + addNewFieldView?.apply { + visibility = View.VISIBLE + setOnClickListener { addNewCustomField() } + } + } + + // Verify the education views + entryEditActivityEducation = EntryEditActivityEducation(this) + entryEditActivityEducation?.let { + Handler().post { performedNextEducation(it) } + } + } + + private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { + if (generatePasswordView != null + && entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( + generatePasswordView!!, + { + openPasswordGenerator() + }, + { + performedNextEducation(entryEditActivityEducation) + } + )) + else if (mEntry != null + && mEntry!!.allowExtraFields() + && !mEntry!!.containsCustomFields() + && addNewFieldView != null + && entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( + addNewFieldView!!, + { + addNewCustomField() + })) + ; + } + + /** + * Open the password generator fragment + */ + private fun openPasswordGenerator() { + GeneratePasswordDialogFragment().show(supportFragmentManager, "PasswordGeneratorFragment") + } + + /** + * Add a new view to fill in the information of the customized field + */ + private fun addNewCustomField() { + val entryEditCustomField = EntryEditCustomField(this@EntryEditActivity) + entryEditCustomField.setData("", ProtectedString(false, "")) + val visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this) + entryEditCustomField.setFontVisibility(visibilityFontActivated) + entryExtraFieldsContainer?.addView(entryEditCustomField) + + // Scroll bottom + scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) } + } + + /** + * Saves the new entry or update an existing entry in the database + */ + private fun saveEntry(entry: EntryVersioned) { + + // Launch a validation and show the error if present + if (!isValid()) { + if (validationErrorMessageId != UNKNOWN_MESSAGE) + Toast.makeText(this@EntryEditActivity, validationErrorMessageId, Toast.LENGTH_LONG).show() + return + } + // Clone the entry + mDatabase?.let { database -> + mNewEntry = EntryVersioned(entry) + mNewEntry?.let { newEntry -> + populateEntryWithViewInfo(newEntry) + + // Open a progress dialog and save entry + var task: ActionRunnable? = null + val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() { + override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { + if (actionNodeValues.success) + finish() + } + } + if (mIsNew) { + mParent?.let { parent -> + task = AddEntryRunnable(this@EntryEditActivity, + database, + newEntry, + parent, + afterActionNodeFinishRunnable, + !readOnly) + } + + } else { + task = UpdateEntryRunnable(this@EntryEditActivity, + database, + entry, + newEntry, + afterActionNodeFinishRunnable, + !readOnly) + } + Thread(task).start() + } + } + } + + /** + * Validate or not the entry form + * + * @return ErrorValidation An error with a message or a validation without message + */ + private fun isValid(): Boolean { + + // Require title + if (entryTitleView?.text.toString().isEmpty()) { + validationErrorMessageId = R.string.error_title_required + return false + } + + // Validate password + if (entryPasswordView?.text.toString() != entryConfirmationPasswordView?.text.toString()) { + validationErrorMessageId = R.string.error_pass_match + return false + } + + // Validate extra fields + if (mEntry?.allowExtraFields() == true) { + entryExtraFieldsContainer?.let { + for (i in 0 until it.childCount) { + val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField + val key = entryEditCustomField.label + if (key == null || key.isEmpty()) { + validationErrorMessageId = R.string.error_string_key + return false + } + } + } + } + return true + } + + private fun populateEntryWithViewInfo(newEntry: EntryVersioned) { + + mDatabase?.startManageEntry(newEntry) + + newEntry.lastAccessTime = PwDate() + newEntry.lastModificationTime = PwDate() + + newEntry.title = entryTitleView?.text.toString() + newEntry.icon = retrieveIcon() + + newEntry.url = entryUrlView?.text.toString() + newEntry.username = entryUserNameView?.text.toString() + newEntry.notes = entryCommentView?.text.toString() + newEntry.password = entryPasswordView?.text.toString() + + if (newEntry.allowExtraFields()) { + // Delete all extra strings + newEntry.removeAllCustomFields() + // Add extra fields from views + entryExtraFieldsContainer?.let { + for (i in 0 until it.childCount) { + val view = it.getChildAt(i) as EntryEditCustomField + val key = view.label + val value = view.value + val protect = view.isProtected + newEntry.addExtraField(key, ProtectedString(protect, value)) + } + } + } + + mDatabase?.stopManageEntry(newEntry) + } + + /** + * Retrieve the icon by the selection, or the first icon in the list if the entry is new or the last one + */ + private fun retrieveIcon(): PwIcon { + + return if (mSelectedIconStandard?.isUnknown != true) + mSelectedIconStandard + else { + if (mIsNew) { + mDatabase?.iconFactory?.keyIcon + } else { + // Keep previous icon, if no new one was selected + mEntry?.icon + } + } ?: PwIconStandard() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + + val inflater = menuInflater + inflater.inflate(R.menu.database_lock, menu) + MenuUtil.contributionMenuInflater(inflater, menu) + + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_lock -> { + lockAndExit() + return true + } + + R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this) + + android.R.id.home -> finish() + } + + return super.onOptionsItemSelected(item) + } + + private fun assignIconView() { + mEntry?.icon?.let { + mDatabase?.drawFactory?.assignDatabaseIconTo( + this, + entryIconView, + it, + iconColor) + } + } + + private fun fillEntryDataInContentsView(entry: EntryVersioned) { + + assignIconView() + + // Don't start the field reference manager, we want to see the raw ref + mDatabase?.stopManageEntry(entry) + + entryTitleView?.setText(entry.title) + entryUserNameView?.setText(entry.username) + entryUrlView?.setText(entry.url) + val password = entry.password + entryPasswordView?.setText(password) + entryConfirmationPasswordView?.setText(password) + entryCommentView?.setText(entry.notes) + + val visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this) + if (visibilityFontActivated) { + Util.applyFontVisibilityTo(this, entryUserNameView) + Util.applyFontVisibilityTo(this, entryPasswordView) + Util.applyFontVisibilityTo(this, entryConfirmationPasswordView) + Util.applyFontVisibilityTo(this, entryCommentView) + } + + if (entry.allowExtraFields()) { + val container = findViewById(R.id.entry_edit_advanced_container) + entry.fields.doActionToAllCustomProtectedField { key, value -> + val entryEditCustomField = EntryEditCustomField(this@EntryEditActivity) + entryEditCustomField.setData(key, value) + entryEditCustomField.setFontVisibility(visibilityFontActivated) + container.addView(entryEditCustomField) + } + } + } + + override fun iconPicked(bundle: Bundle) { + mSelectedIconStandard = bundle.getParcelable(KEY_ICON_STANDARD) + mSelectedIconStandard?.let { + mEntry?.icon = it + } + assignIconView() + } + + override fun onSaveInstanceState(outState: Bundle) { + if (!mSelectedIconStandard!!.isUnknown) { + outState.putParcelable(KEY_ICON_STANDARD, mSelectedIconStandard) + super.onSaveInstanceState(outState) + } + } + + override fun acceptPassword(bundle: Bundle) { + bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let { + entryPasswordView?.setText(it) + entryConfirmationPasswordView?.setText(it) + } + + entryEditActivityEducation?.let { + Handler().post { performedNextEducation(it) } + } + } + + override fun cancelPassword(bundle: Bundle) { + // Do nothing here + } + + override fun finish() { + // Assign entry callback as a result in all case + try { + mNewEntry?.let { + val bundle = Bundle() + val intentEntry = Intent() + bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry) + intentEntry.putExtras(bundle) + if (mIsNew) { + setResult(ADD_ENTRY_RESULT_CODE, intentEntry) + } else { + setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry) + } + } + super.finish() + } catch (e: Exception) { + // Exception when parcelable can't be done + Log.e(TAG, "Cant add entry as result", e) + } + } + + companion object { + + private val TAG = EntryEditActivity::class.java.name + + // Keys for current Activity + const val KEY_ENTRY = "entry" + const val KEY_PARENT = "parent" + + // Keys for callback + const val ADD_ENTRY_RESULT_CODE = 31 + const val UPDATE_ENTRY_RESULT_CODE = 32 + const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129 + const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY" + + const val UNKNOWN_MESSAGE = -1 + + /** + * Launch EntryEditActivity to update an existing entry + * + * @param activity from activity + * @param pwEntry Entry to update + */ + fun launch(activity: Activity, pwEntry: EntryVersioned) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { + val intent = Intent(activity, EntryEditActivity::class.java) + intent.putExtra(KEY_ENTRY, pwEntry.nodeId) + activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE) + } + } + + /** + * Launch EntryEditActivity to add a new entry + * + * @param activity from activity + * @param pwGroup Group who will contains new entry + */ + fun launch(activity: Activity, pwGroup: GroupVersioned) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { + val intent = Intent(activity, EntryEditActivity::class.java) + intent.putExtra(KEY_PARENT, pwGroup.nodeId) + activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE) + } + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java index 4a0266b63..931019011 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java @@ -298,7 +298,7 @@ public class GroupActivity extends LockingActivity .show(getSupportFragmentManager(), GroupEditDialogFragment.TAG_CREATE_GROUP)); addNodeButtonView.setAddEntryClickListener(v -> - EntryEditActivity.launch(GroupActivity.this, mCurrentGroup)); + EntryEditActivity.Companion.launch(GroupActivity.this, mCurrentGroup)); // Search suggestion searchSuggestionAdapter = new SearchEntryCursorAdapter(this, database); @@ -570,7 +570,7 @@ public class GroupActivity extends LockingActivity GroupEditDialogFragment.TAG_CREATE_GROUP); break; case ENTRY: - EntryEditActivity.launch(GroupActivity.this, (EntryVersioned) node); + EntryEditActivity.Companion.launch(GroupActivity.this, (EntryVersioned) node); break; } return true; diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt index 6849211e3..dfd61bc85 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt @@ -163,9 +163,9 @@ abstract class LockingActivity : StylishActivity() { /** * To reset the app timeout when a view is focused or changed */ - protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View) { + protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) { views.forEach { - it.setOnFocusChangeListener { _, hasFocus -> + it?.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) { TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) } From eda7fa0fe79e24beeb89a40451bb99292f42cbaa Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 7 Jul 2019 18:49:37 +0200 Subject: [PATCH 09/24] Fix NodeHandler parameter --- .../com/kunzisoft/keepass/database/search/SearchDbHelper.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchDbHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchDbHelper.kt index ee3445b34..f10d00120 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchDbHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchDbHelper.kt @@ -48,11 +48,11 @@ class SearchDbHelper(private val isOmitBackup: Boolean) { incrementEntry = 0 database.rootGroup?.doForEachChild( object : NodeHandler() { - override fun operate(entry: EntryVersioned): Boolean { + override fun operate(node: EntryVersioned): Boolean { if (incrementEntry >= max) return false - if (entryContainsString(entry, finalQStr, loc)) { - searchGroup?.addChildEntry(entry) + if (entryContainsString(node, finalQStr, loc)) { + searchGroup?.addChildEntry(node) incrementEntry++ } // Stop searching when we have max entries From d6bac74e1b6409925e2f8d050ed8755df0348fac Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 7 Jul 2019 20:01:17 +0200 Subject: [PATCH 10/24] Kotlinized GroupActivity --- .../keepass/activities/GroupActivity.java | 1135 ----------------- .../keepass/activities/GroupActivity.kt | 1051 +++++++++++++++ .../AssignPasswordInDatabaseRunnable.kt | 5 +- .../FileDatabaseSelectActivity.java | 6 +- .../IntentBuildLauncher.java | 2 +- .../keepass/password/PasswordActivity.java | 7 +- 6 files changed, 1060 insertions(+), 1146 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt rename app/src/main/java/com/kunzisoft/keepass/{activities => password}/IntentBuildLauncher.java (73%) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java deleted file mode 100644 index 931019011..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java +++ /dev/null @@ -1,1135 +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.activities; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Dialog; -import android.app.SearchManager; -import android.app.assist.AssistStructure; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.RequiresApi; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.support.v7.widget.SearchView; -import android.support.v7.widget.Toolbar; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.activities.lock.LockingActivity; -import com.kunzisoft.keepass.adapters.NodeAdapter; -import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter; -import com.kunzisoft.keepass.app.App; -import com.kunzisoft.keepass.autofill.AutofillHelper; -import com.kunzisoft.keepass.database.SortNodeEnum; -import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable; -import com.kunzisoft.keepass.database.action.ProgressDialogRunnable; -import com.kunzisoft.keepass.database.action.node.ActionNodeValues; -import com.kunzisoft.keepass.database.action.node.AddGroupRunnable; -import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable; -import com.kunzisoft.keepass.database.action.node.CopyEntryRunnable; -import com.kunzisoft.keepass.database.action.node.DeleteEntryRunnable; -import com.kunzisoft.keepass.database.action.node.DeleteGroupRunnable; -import com.kunzisoft.keepass.database.action.node.MoveEntryRunnable; -import com.kunzisoft.keepass.database.action.node.MoveGroupRunnable; -import com.kunzisoft.keepass.database.action.node.UpdateGroupRunnable; -import com.kunzisoft.keepass.database.element.Database; -import com.kunzisoft.keepass.database.element.EntryVersioned; -import com.kunzisoft.keepass.database.element.GroupVersioned; -import com.kunzisoft.keepass.database.element.NodeVersioned; -import com.kunzisoft.keepass.database.element.PwIcon; -import com.kunzisoft.keepass.database.element.PwNodeId; -import com.kunzisoft.keepass.database.element.security.ProtectedString; -import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment; -import com.kunzisoft.keepass.dialogs.GroupEditDialogFragment; -import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment; -import com.kunzisoft.keepass.dialogs.PasswordEncodingDialogHelper; -import com.kunzisoft.keepass.dialogs.ReadOnlyDialog; -import com.kunzisoft.keepass.dialogs.SortDialogFragment; -import com.kunzisoft.keepass.education.GroupActivityEducation; -import com.kunzisoft.keepass.magikeyboard.KeyboardEntryNotificationService; -import com.kunzisoft.keepass.magikeyboard.KeyboardHelper; -import com.kunzisoft.keepass.magikeyboard.MagikIME; -import com.kunzisoft.keepass.model.Entry; -import com.kunzisoft.keepass.model.Field; -import com.kunzisoft.keepass.settings.PreferencesUtil; -import com.kunzisoft.keepass.timeout.TimeoutHelper; -import com.kunzisoft.keepass.utils.MenuUtil; -import com.kunzisoft.keepass.view.AddNodeButtonView; - -import net.cachapa.expandablelayout.ExpandableLayout; - -import org.jetbrains.annotations.NotNull; - -import kotlin.Unit; -import kotlin.jvm.functions.Function2; - -public class GroupActivity extends LockingActivity - implements GroupEditDialogFragment.EditGroupListener, - IconPickerDialogFragment.IconPickerListener, - NodeAdapter.NodeMenuListener, - ListNodesFragment.OnScrollListener, - AssignMasterKeyDialogFragment.AssignPasswordDialogListener, - NodeAdapter.NodeClickCallback, - SortDialogFragment.SortSelectionListener { - - private static final String TAG = GroupActivity.class.getName(); - - private static final String GROUP_ID_KEY = "GROUP_ID_KEY"; - private static final String LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"; - private static final String SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"; - private static final String OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"; - private static final String NODE_TO_COPY_KEY = "NODE_TO_COPY_KEY"; - private static final String NODE_TO_MOVE_KEY = "NODE_TO_MOVE_KEY"; - - private Toolbar toolbar; - private View searchTitleView; - private ExpandableLayout toolbarPasteExpandableLayout; - private Toolbar toolbarPaste; - private ImageView iconView; - private TextView modeTitleView; - private AddNodeButtonView addNodeButtonView; - private TextView groupNameView; - - private Database database; - - private ListNodesFragment listNodesFragment; - private boolean currentGroupIsASearch; - - private GroupVersioned rootGroup; - private GroupVersioned mCurrentGroup; - private GroupVersioned oldGroupToUpdate; - private NodeVersioned nodeToCopy; - private NodeVersioned nodeToMove; - - private SearchEntryCursorAdapter searchSuggestionAdapter; - - private int iconColor; - - private static void buildAndLaunchIntent(Activity activity, GroupVersioned group, boolean readOnly, - IntentBuildLauncher intentBuildLauncher) { - if (TimeoutHelper.INSTANCE.checkTimeAndLockIfTimeout(activity)) { - Intent intent = new Intent(activity, GroupActivity.class); - if (group != null) { - intent.putExtra(GROUP_ID_KEY, group.getNodeId()); - } - ReadOnlyHelper.INSTANCE.putReadOnlyInIntent(intent, readOnly); - intentBuildLauncher.launchActivity(intent); - } - } - - /* - * ------------------------- - * Standard Launch - * ------------------------- - */ - - public static void launch(Activity activity) { - launch(activity, PreferencesUtil.enableReadOnlyDatabase(activity)); - } - - public static void launch(Activity activity, boolean readOnly) { - launch(activity, null, readOnly); - } - - public static void launch(Activity activity, GroupVersioned group, boolean readOnly) { - TimeoutHelper.INSTANCE.recordTime(activity); - buildAndLaunchIntent(activity, group, readOnly, - (intent) -> activity.startActivityForResult(intent, 0)); - } - - - /* - * ------------------------- - * Keyboard Launch - * ------------------------- - */ - // TODO implement pre search to directly open the direct group - - public static void launchForKeyboardSelection(Activity activity, boolean readOnly) { - TimeoutHelper.INSTANCE.recordTime(activity); - buildAndLaunchIntent(activity, null, readOnly, - (intent) -> KeyboardHelper.INSTANCE.startActivityForKeyboardSelection(activity, intent)); - } - - /* - * ------------------------- - * Autofill Launch - * ------------------------- - */ - // TODO implement pre search to directly open the direct group - - @RequiresApi(api = Build.VERSION_CODES.O) - public static void launchForAutofillResult(Activity activity, @NonNull AssistStructure assistStructure, boolean readOnly) { - TimeoutHelper.INSTANCE.recordTime(activity); - buildAndLaunchIntent(activity, null, readOnly, - (intent) -> AutofillHelper.INSTANCE.startActivityForAutofillResult(activity, intent, assistStructure)); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (isFinishing()) { - return; - } - - database = App.Companion.getCurrentDatabase(); - - // Construct main view - setContentView(getLayoutInflater().inflate(R.layout.list_nodes_with_add_button, null)); - - // Initialize views - iconView = findViewById(R.id.icon); - addNodeButtonView = findViewById(R.id.add_node_button); - toolbar = findViewById(R.id.toolbar); - searchTitleView = findViewById(R.id.search_title); - groupNameView = findViewById(R.id.group_name); - toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout); - toolbarPaste = findViewById(R.id.toolbar_paste); - modeTitleView = findViewById(R.id.mode_title_view); - - // Focus view to reinitialize timeout - resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView); - - // Retrieve elements after an orientation change - if (savedInstanceState != null) { - if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY)) - oldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY); - - if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) { - nodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY); - toolbarPaste.setOnMenuItemClickListener(new OnCopyMenuItemClickListener()); - } - else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) { - nodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY); - toolbarPaste.setOnMenuItemClickListener(new OnMoveMenuItemClickListener()); - } - } - - try { - rootGroup = database.getRootGroup(); - } catch (NullPointerException e) { - Log.e(TAG, "Unable to get rootGroup"); - } - mCurrentGroup = retrieveCurrentGroup(getIntent(), savedInstanceState); - currentGroupIsASearch = Intent.ACTION_SEARCH.equals(getIntent().getAction()); - - Log.i(TAG, "Started creating tree"); - if ( mCurrentGroup == null ) { - Log.w(TAG, "Group was null"); - return; - } - - // Update last access time. - mCurrentGroup.touch(false, false); - - toolbar.setTitle(""); - setSupportActionBar(toolbar); - - toolbarPaste.inflateMenu(R.menu.node_paste_menu); - toolbarPaste.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp); - toolbarPaste.setNavigationOnClickListener(view -> { - toolbarPasteExpandableLayout.collapse(); - nodeToCopy = null; - nodeToMove = null; - }); - - // Retrieve the textColor to tint the icon - int[] attrs = {R.attr.textColorInverse}; - TypedArray ta = getTheme().obtainStyledAttributes(attrs); - iconColor = ta.getColor(0, Color.WHITE); - - String fragmentTag = LIST_NODES_FRAGMENT_TAG; - if (currentGroupIsASearch) - fragmentTag = SEARCH_FRAGMENT_TAG; - - // Initialize the fragment with the list - listNodesFragment = (ListNodesFragment) getSupportFragmentManager() - .findFragmentByTag(fragmentTag); - if (listNodesFragment == null) - listNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, getReadOnly(), currentGroupIsASearch); - - // Attach fragment to content view - getSupportFragmentManager().beginTransaction().replace( - R.id.nodes_list_fragment_container, - listNodesFragment, - fragmentTag) - .commit(); - - // Add listeners to the add buttons - addNodeButtonView.setAddGroupClickListener(v -> GroupEditDialogFragment.build() - .show(getSupportFragmentManager(), - GroupEditDialogFragment.TAG_CREATE_GROUP)); - addNodeButtonView.setAddEntryClickListener(v -> - EntryEditActivity.Companion.launch(GroupActivity.this, mCurrentGroup)); - - // Search suggestion - searchSuggestionAdapter = new SearchEntryCursorAdapter(this, database); - - Log.i(TAG, "Finished creating tree"); - } - - @Override - protected void onNewIntent(Intent intent) { - Log.d(TAG, "setNewIntent: " + intent.toString()); - setIntent(intent); - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - // only one instance of search in backstack - openSearchGroup(retrieveCurrentGroup(intent, null)); - currentGroupIsASearch = true; - } else { - currentGroupIsASearch = false; - } - } - - private void openSearchGroup(GroupVersioned group) { - // Delete the previous search fragment - Fragment searchFragment = getSupportFragmentManager().findFragmentByTag(SEARCH_FRAGMENT_TAG); - if (searchFragment != null) { - if ( getSupportFragmentManager() - .popBackStackImmediate(SEARCH_FRAGMENT_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) ) - getSupportFragmentManager().beginTransaction().remove(searchFragment).commit(); - } - - openGroup(group, true); - } - - private void openChildGroup(GroupVersioned group) { - openGroup(group, false); - } - - private void openGroup(GroupVersioned group, boolean isASearch) { - // Check TimeoutHelper - TimeoutHelper.INSTANCE.checkTimeAndLockIfTimeoutOrResetTimeout(this, () -> { - // Open a group in a new fragment - ListNodesFragment newListNodeFragment = ListNodesFragment.newInstance(group, getReadOnly(), isASearch); - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - // Different animation - String fragmentTag; - if (isASearch) { - fragmentTransaction.setCustomAnimations(R.anim.slide_in_top, R.anim.slide_out_bottom, - R.anim.slide_in_bottom, R.anim.slide_out_top); - fragmentTag = SEARCH_FRAGMENT_TAG; - } else { - fragmentTransaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, - R.anim.slide_in_left, R.anim.slide_out_right); - fragmentTag = LIST_NODES_FRAGMENT_TAG; - } - - fragmentTransaction.replace(R.id.nodes_list_fragment_container, - newListNodeFragment, - fragmentTag); - fragmentTransaction.addToBackStack(fragmentTag); - fragmentTransaction.commit(); - - listNodesFragment = newListNodeFragment; - mCurrentGroup = group; - assignGroupViewElements(); - return null; - }); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - if (mCurrentGroup != null) - outState.putParcelable(GROUP_ID_KEY, mCurrentGroup.getNodeId()); - outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, oldGroupToUpdate); - if (nodeToCopy != null) - outState.putParcelable(NODE_TO_COPY_KEY, nodeToCopy); - if (nodeToMove != null) - outState.putParcelable(NODE_TO_MOVE_KEY, nodeToMove); - super.onSaveInstanceState(outState); - } - - protected @Nullable GroupVersioned retrieveCurrentGroup(Intent intent, @Nullable Bundle savedInstanceState) { - - // If it's a search - if ( Intent.ACTION_SEARCH.equals(intent.getAction()) ) { - return database.search(intent.getStringExtra(SearchManager.QUERY).trim()); - } - // else a real group - else { - PwNodeId pwGroupId = null; - if (savedInstanceState != null - && savedInstanceState.containsKey(GROUP_ID_KEY)) { - pwGroupId = savedInstanceState.getParcelable(GROUP_ID_KEY); - } else { - if (getIntent() != null) - pwGroupId = intent.getParcelableExtra(GROUP_ID_KEY); - } - - setReadOnly(database.isReadOnly() || getReadOnly()); // Force read only if the database is like that - - Log.w(TAG, "Creating tree view"); - GroupVersioned currentGroup; - if (pwGroupId == null) { - currentGroup = rootGroup; - } else { - currentGroup = database.getGroupById(pwGroupId); - } - - return currentGroup; - } - } - - public void assignGroupViewElements() { - // Assign title - if (mCurrentGroup != null) { - String title = mCurrentGroup.getTitle(); - if (title != null && title.length() > 0) { - if (groupNameView != null) { - groupNameView.setText(title); - groupNameView.invalidate(); - } - } else { - if (groupNameView != null) { - groupNameView.setText(getText(R.string.root)); - groupNameView.invalidate(); - } - } - } - if (currentGroupIsASearch) { - searchTitleView.setVisibility(View.VISIBLE); - } else { - searchTitleView.setVisibility(View.GONE); - } - - // Assign icon - if (currentGroupIsASearch) { - if (toolbar != null) { - toolbar.setNavigationIcon(null); - } - iconView.setVisibility(View.GONE); - } else { - // Assign the group icon depending of IconPack or custom icon - iconView.setVisibility(View.VISIBLE); - if (mCurrentGroup != null) { - database.getDrawFactory().assignDatabaseIconTo(this, iconView, mCurrentGroup.getIcon(), iconColor); - - if (toolbar != null) { - if (mCurrentGroup.containsParent()) - toolbar.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp); - else { - toolbar.setNavigationIcon(null); - } - } - } - } - - // Show selection mode message if needed - if (getSelectionMode()) { - modeTitleView.setVisibility(View.VISIBLE); - } else { - modeTitleView.setVisibility(View.GONE); - } - - // Show button if allowed - if (addNodeButtonView != null) { - - // To enable add button - boolean addGroupEnabled = !getReadOnly() && !currentGroupIsASearch; - boolean addEntryEnabled = !getReadOnly() && !currentGroupIsASearch; - if (mCurrentGroup != null) { - boolean isRoot = (mCurrentGroup != null && mCurrentGroup == rootGroup); - if (!mCurrentGroup.allowAddEntryIfIsRoot()) - addEntryEnabled = !isRoot && addEntryEnabled; - if (isRoot) { - showWarnings(); - } - } - addNodeButtonView.enableAddGroup(addGroupEnabled); - addNodeButtonView.enableAddEntry(addEntryEnabled); - - if (addNodeButtonView.isEnable()) - addNodeButtonView.showButton(); - } - } - - @Override - public void onScrolled(int dy) { - if (addNodeButtonView != null) - addNodeButtonView.hideButtonOnScrollListener(dy); - } - - @Override - public void onNodeClick(NodeVersioned node) { - switch (node.getType()) { - case GROUP: - try { - openChildGroup((GroupVersioned) node); - } catch (ClassCastException e) { - Log.e(TAG, "Node can't be cast in Group"); - } - break; - case ENTRY: - try { - EntryVersioned entry = ((EntryVersioned) node); - EntrySelectionHelper.INSTANCE.doEntrySelectionAction(getIntent(), - () -> { - EntryActivity.Companion.launch(GroupActivity.this, entry, getReadOnly()); - return null; - }, - () -> { - MagikIME.setEntryKey(getEntry(entry)); - // Show the notification if allowed in Preferences - if (PreferencesUtil.enableKeyboardNotificationEntry(GroupActivity.this)) { - startService(new Intent( - GroupActivity.this, - KeyboardEntryNotificationService.class)); - } - // Consume the selection mode - EntrySelectionHelper.INSTANCE.removeEntrySelectionModeFromIntent(getIntent()); - moveTaskToBack(true); - return null; - }, - assistStructure -> { - // Build response with the entry selected - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.INSTANCE.buildResponseWhenEntrySelected(GroupActivity.this, entry); - } - finish(); - return null; - }); - } catch (ClassCastException e) { - Log.e(TAG, "Node can't be cast in Entry"); - } - break; - } - } - - private Entry getEntry(EntryVersioned entry) { - Entry entryModel = new Entry(); - entryModel.setTitle(entry.getTitle()); - entryModel.setUsername(entry.getUsername()); - entryModel.setPassword(entry.getPassword()); - entryModel.setUrl(entry.getUrl()); - if (entry.containsCustomFields()) { - entry.getFields() - .doActionToAllCustomProtectedField(new Function2() { - @Override - public Unit invoke(String key, ProtectedString value) { - entryModel.addCustomField( - new Field(key, value.toString())); - return null; - } - }); - } - return entryModel; - } - - @Override - public boolean onOpenMenuClick(NodeVersioned node) { - onNodeClick(node); - return true; - } - - @Override - public boolean onEditMenuClick(NodeVersioned node) { - switch (node.getType()) { - case GROUP: - oldGroupToUpdate = (GroupVersioned) node; - GroupEditDialogFragment.build(oldGroupToUpdate) - .show(getSupportFragmentManager(), - GroupEditDialogFragment.TAG_CREATE_GROUP); - break; - case ENTRY: - EntryEditActivity.Companion.launch(GroupActivity.this, (EntryVersioned) node); - break; - } - return true; - } - - @Override - public boolean onCopyMenuClick(NodeVersioned node) { - - toolbarPasteExpandableLayout.expand(); - nodeToCopy = node; - toolbarPaste.setOnMenuItemClickListener(new OnCopyMenuItemClickListener()); - return false; - } - - private class OnCopyMenuItemClickListener implements Toolbar.OnMenuItemClickListener{ - - @Override - public boolean onMenuItemClick(MenuItem item) { - toolbarPasteExpandableLayout.collapse(); - - switch (item.getItemId()) { - case R.id.menu_paste: - switch (nodeToCopy.getType()) { - case GROUP: - Log.e(TAG, "Copy not allowed for group"); - break; - case ENTRY: - copyEntry((EntryVersioned) nodeToCopy, mCurrentGroup); - break; - } - nodeToCopy = null; - return true; - } - return true; - } - } - - private void copyEntry(EntryVersioned entryToCopy, GroupVersioned newParent) { - new Thread(new CopyEntryRunnable(this, - App.Companion.getCurrentDatabase(), - entryToCopy, - newParent, - new AfterAddNodeRunnable(), - !getReadOnly()) - ).start(); - } - - @Override - public boolean onMoveMenuClick(NodeVersioned node) { - - toolbarPasteExpandableLayout.expand(); - nodeToMove = node; - toolbarPaste.setOnMenuItemClickListener(new OnMoveMenuItemClickListener()); - return false; - } - - private class OnMoveMenuItemClickListener implements Toolbar.OnMenuItemClickListener{ - - @Override - public boolean onMenuItemClick(MenuItem item) { - toolbarPasteExpandableLayout.collapse(); - - switch (item.getItemId()) { - case R.id.menu_paste: - switch (nodeToMove.getType()) { - case GROUP: - moveGroup((GroupVersioned) nodeToMove, mCurrentGroup); - break; - case ENTRY: - moveEntry((EntryVersioned) nodeToMove, mCurrentGroup); - break; - } - nodeToMove = null; - return true; - } - return true; - } - } - - private void moveGroup(GroupVersioned groupToMove, GroupVersioned newParent) { - new Thread(new MoveGroupRunnable( - this, - App.Companion.getCurrentDatabase(), - groupToMove, - newParent, - new AfterAddNodeRunnable(), - !getReadOnly()) - ).start(); - } - - private void moveEntry(EntryVersioned entryToMove, GroupVersioned newParent) { - new Thread(new MoveEntryRunnable( - this, - App.Companion.getCurrentDatabase(), - entryToMove, - newParent, - new AfterAddNodeRunnable(), - !getReadOnly()) - ).start(); - } - - @Override - public boolean onDeleteMenuClick(NodeVersioned node) { - switch (node.getType()) { - case GROUP: - deleteGroup((GroupVersioned) node); - break; - case ENTRY: - deleteEntry((EntryVersioned) node); - break; - } - return true; - } - - private void deleteGroup(GroupVersioned group) { - //TODO Verify trash recycle bin - new Thread(new DeleteGroupRunnable( - this, - App.Companion.getCurrentDatabase(), - group, - new AfterDeleteNodeRunnable(), - !getReadOnly()) - ).start(); - } - - private void deleteEntry(EntryVersioned entry) { - new Thread(new DeleteEntryRunnable( - this, - App.Companion.getCurrentDatabase(), - entry, - new AfterDeleteNodeRunnable(), - !getReadOnly()) - ).start(); - } - - @Override - protected void onResume() { - super.onResume(); - // Refresh the elements - assignGroupViewElements(); - // Refresh suggestions to change preferences - if (searchSuggestionAdapter != null) - searchSuggestionAdapter.reInit(this); - } - - @Override - protected void onStop() { - super.onStop(); - // Hide button - if (addNodeButtonView != null) - addNodeButtonView.hideButton(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.search, menu); - inflater.inflate(R.menu.database_lock, menu); - if (!getReadOnly()) - inflater.inflate(R.menu.database_master_key, menu); - if (!getSelectionMode()) { - inflater.inflate(R.menu.default_menu, menu); - MenuUtil.INSTANCE.contributionMenuInflater(inflater, menu); - } - - // Get the SearchView and set the searchable configuration - SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); - assert searchManager != null; - - MenuItem searchItem = menu.findItem(R.id.menu_search); - SearchView searchView = null; - if (searchItem != null) { - searchView = (SearchView) searchItem.getActionView(); - } - if (searchView != null) { - searchView.setSearchableInfo(searchManager.getSearchableInfo(new ComponentName(this, GroupActivity.class))); - searchView.setIconifiedByDefault(false); // Do not iconify the widget; expand it by default - searchView.setSuggestionsAdapter(searchSuggestionAdapter); - searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { - @Override - public boolean onSuggestionClick(int position) { - onNodeClick(searchSuggestionAdapter.getEntryFromPosition(position)); - return true; - } - - @Override - public boolean onSuggestionSelect(int position) { - return true; - } - }); - } - - super.onCreateOptionsMenu(menu); - - // Launch education screen - new Handler().post(() -> performedNextEducation(new GroupActivityEducation(this), menu)); - - return true; - } - - private void performedNextEducation(GroupActivityEducation groupActivityEducation, - Menu menu) { - // If no node, show education to add new one - if (listNodesFragment != null - && listNodesFragment.isEmpty() - && addNodeButtonView.isEnable() - && groupActivityEducation.checkAndPerformedAddNodeButtonEducation( - addNodeButtonView, - tapTargetView -> { - addNodeButtonView.openButtonIfClose(); - return null; - }, - tapTargetView -> { - performedNextEducation(groupActivityEducation, menu); - return null; - } - )); - else if (toolbar.findViewById(R.id.menu_search) != null - && groupActivityEducation.checkAndPerformedSearchMenuEducation( - toolbar.findViewById(R.id.menu_search), - tapTargetView -> { - menu.findItem(R.id.menu_search).expandActionView(); - return null; - }, - tapTargetView -> { - performedNextEducation(groupActivityEducation, menu); - return null; - })); - else if (toolbar.findViewById(R.id.menu_sort) != null - && groupActivityEducation.checkAndPerformedSortMenuEducation( - toolbar.findViewById(R.id.menu_sort), - tapTargetView -> { - onOptionsItemSelected(menu.findItem(R.id.menu_sort)); - return null; - }, - tapTargetView -> { - performedNextEducation(groupActivityEducation, menu); - return null; - })); - else if (toolbar.findViewById(R.id.menu_lock) != null - && groupActivityEducation.checkAndPerformedLockMenuEducation( - toolbar.findViewById(R.id.menu_lock), - tapTargetView -> { - onOptionsItemSelected(menu.findItem(R.id.menu_lock)); - return null; - }, - tapTargetView -> { - performedNextEducation(groupActivityEducation, menu); - return null; - })); - } - - @Override - public void startActivity(Intent intent) { - - // Get the intent, verify the action and get the query - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - String query = intent.getStringExtra(SearchManager.QUERY); - // manually launch the real search activity - final Intent searchIntent = new Intent(getApplicationContext(), GroupActivity.class); - // add query to the Intent Extras - searchIntent.setAction(Intent.ACTION_SEARCH); - searchIntent.putExtra(SearchManager.QUERY, query); - - EntrySelectionHelper.INSTANCE.doEntrySelectionAction(intent, - () -> { - GroupActivity.super.startActivity(intent); - return null; - }, - () -> { - KeyboardHelper.INSTANCE.startActivityForKeyboardSelection( - GroupActivity.this, - searchIntent); - finish(); - return null; - }, - assistStructure -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.INSTANCE.startActivityForAutofillResult( - GroupActivity.this, - searchIntent, - assistStructure); - } - return null; - }); - } else { - super.startActivity(intent); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - - case android.R.id.home: - onBackPressed(); - return true; - - case R.id.menu_search: - //onSearchRequested(); - return true; - - case R.id.menu_lock: - lockAndExit(); - return true; - - case R.id.menu_change_master_key: - setPassword(); - return true; - default: - // Check the time lock before launching settings - MenuUtil.INSTANCE.onDefaultMenuOptionsItemSelected(this, item, getReadOnly(), true); - return super.onOptionsItemSelected(item); - } - } - - private void setPassword() { - AssignMasterKeyDialogFragment dialog = new AssignMasterKeyDialogFragment(); - dialog.show(getSupportFragmentManager(), "passwordDialog"); - } - - @Override - public void approveEditGroup(GroupEditDialogFragment.EditGroupDialogAction action, - String name, - PwIcon icon) { - Database database = App.Companion.getCurrentDatabase(); - - switch (action) { - case CREATION: - // If group creation - // Build the group - GroupVersioned newGroup = database.createGroup(); - newGroup.setTitle(name); - newGroup.setIcon(icon); - // Not really needed here because added in runnable but safe - newGroup.setParent(mCurrentGroup); - - // If group created save it in the database - new Thread(new AddGroupRunnable(this, - App.Companion.getCurrentDatabase(), - newGroup, - mCurrentGroup, - new AfterAddNodeRunnable(), - !getReadOnly()) - ).start(); - - break; - case UPDATE: - // If update add new elements - if (oldGroupToUpdate != null) { - GroupVersioned updateGroup = new GroupVersioned(oldGroupToUpdate); - updateGroup.setTitle(name); - // TODO custom icon - updateGroup.setIcon(icon); - - if (listNodesFragment != null) - listNodesFragment.removeNode(oldGroupToUpdate); - - // If group updated save it in the database - new Thread(new UpdateGroupRunnable(this, - App.Companion.getCurrentDatabase(), - oldGroupToUpdate, - updateGroup, - new AfterUpdateNodeRunnable(), - !getReadOnly()) - ).start(); - } - - break; - } - } - - class AfterAddNodeRunnable extends AfterActionNodeFinishRunnable { - - @Override - public void onActionNodeFinish(@NotNull ActionNodeValues actionNodeValues) { - runOnUiThread(() -> { - if (actionNodeValues.getSuccess()) { - if (listNodesFragment != null) - listNodesFragment.addNode(actionNodeValues.getNewNode()); - } - }); - } - } - - class AfterUpdateNodeRunnable extends AfterActionNodeFinishRunnable { - - @Override - public void onActionNodeFinish(@NotNull ActionNodeValues actionNodeValues) { - runOnUiThread(() -> { - if (actionNodeValues.getSuccess()) { - if (listNodesFragment != null) - listNodesFragment.updateNode(actionNodeValues.getOldNode(), actionNodeValues.getNewNode()); - } - }); - } - } - - class AfterDeleteNodeRunnable extends AfterActionNodeFinishRunnable { - - @Override - public void onActionNodeFinish(@NotNull ActionNodeValues actionNodeValues) { - runOnUiThread(() -> { - if (actionNodeValues.getSuccess()) { - - if (listNodesFragment != null) - listNodesFragment.removeNode(actionNodeValues.getOldNode()); - - if (actionNodeValues.getOldNode() != null) { - GroupVersioned parent = actionNodeValues.getOldNode().getParent(); - Database database = App.Companion.getCurrentDatabase(); - if (database.isRecycleBinAvailable() && - database.isRecycleBinEnabled()) { - GroupVersioned recycleBin = database.getRecycleBin(); - // Add trash if it doesn't exists - if (parent.equals(recycleBin) - && mCurrentGroup != null - && mCurrentGroup.getParent() == null - && !mCurrentGroup.equals(recycleBin)) { - - if (listNodesFragment != null) - listNodesFragment.addNode(parent); - } - } - } - } - }); - } - } - - @Override - public void cancelEditGroup(GroupEditDialogFragment.EditGroupDialogAction action, - String name, - PwIcon iconId) { - // Do nothing here - } - - @Override - // For icon in create tree dialog - public void iconPicked(Bundle bundle) { - GroupEditDialogFragment groupEditDialogFragment = - (GroupEditDialogFragment) getSupportFragmentManager() - .findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP); - if (groupEditDialogFragment != null) { - groupEditDialogFragment.iconPicked(bundle); - } - } - - protected void showWarnings() { - if (App.Companion.getCurrentDatabase().isReadOnly()) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - - if (prefs.getBoolean(getString(R.string.show_read_only_warning), true)) { - Dialog dialog = new ReadOnlyDialog(this); - dialog.show(); - } - } - } - - @Override - public void onAssignKeyDialogPositiveClick( - boolean masterPasswordChecked, String masterPassword, - boolean keyFileChecked, Uri keyFile) { - - Thread taskThread = new Thread(new ProgressDialogRunnable(this, - R.string.saving_database, - progressTaskUpdater -> { - return new AssignPasswordInDatabaseRunnable(GroupActivity.this, - database, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile, - true); // TODO save - } - )); - // Show the progress dialog now or after dialog confirmation - if (database.validatePasswordEncoding(masterPassword)) { - taskThread.start(); - } else { - new PasswordEncodingDialogHelper() - .show(this, (dialog, which) -> taskThread.start()); - } - } - - @Override - public void onAssignKeyDialogNegativeClick( - boolean masterPasswordChecked, String masterPassword, - boolean keyFileChecked, Uri keyFile) { - - } - - @Override - public void onSortSelected(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore, boolean recycleBinBottom) { - if (listNodesFragment != null) - listNodesFragment.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.INSTANCE.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data); - } - - // Not directly get the entry from intent data but from database - // Is refresh from onResume() - } - - @SuppressLint("RestrictedApi") - @Override - public void startActivityForResult(Intent intent, int requestCode, Bundle options) { - /* - * ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in - * another app such as Files or GoogleDrive and then Search for an entry. Here we remove the - * FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task. - */ - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - int flags = intent.getFlags(); - flags &= ~Intent.FLAG_ACTIVITY_NEW_TASK; - intent.setFlags(flags); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - super.startActivityForResult(intent, requestCode, options); - } - } - - private void removeSearchInIntent(Intent intent) { - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - currentGroupIsASearch = false; - intent.setAction(Intent.ACTION_DEFAULT); - intent.removeExtra(SearchManager.QUERY); - } - } - - @Override - public void onBackPressed() { - - // Normal way when we are not in root - if (rootGroup!= null && !rootGroup.equals(mCurrentGroup)) - super.onBackPressed(); - // Else lock if needed - else { - if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) { - App.Companion.getCurrentDatabase().closeAndClear(getApplicationContext()); - super.onBackPressed(); - } else { - moveTaskToBack(true); - } - } - - listNodesFragment = (ListNodesFragment) getSupportFragmentManager().findFragmentByTag(LIST_NODES_FRAGMENT_TAG); - // to refresh fragment - listNodesFragment.rebuildList(); - mCurrentGroup = listNodesFragment.getMainGroup(); - removeSearchInIntent(getIntent()); - assignGroupViewElements(); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt new file mode 100644 index 000000000..55bc7e07d --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -0,0 +1,1051 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + */ +package com.kunzisoft.keepass.activities + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.SearchManager +import android.app.assist.AssistStructure +import android.content.ComponentName +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.graphics.Color +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.v4.app.FragmentManager +import android.support.v7.widget.SearchView +import android.support.v7.widget.Toolbar +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import android.widget.TextView + +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.lock.LockingActivity +import com.kunzisoft.keepass.adapters.NodeAdapter +import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter +import com.kunzisoft.keepass.app.App +import com.kunzisoft.keepass.autofill.AutofillHelper +import com.kunzisoft.keepass.database.SortNodeEnum +import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable +import com.kunzisoft.keepass.database.action.ProgressDialogRunnable +import com.kunzisoft.keepass.database.action.node.ActionNodeValues +import com.kunzisoft.keepass.database.action.node.AddGroupRunnable +import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable +import com.kunzisoft.keepass.database.action.node.CopyEntryRunnable +import com.kunzisoft.keepass.database.action.node.DeleteEntryRunnable +import com.kunzisoft.keepass.database.action.node.DeleteGroupRunnable +import com.kunzisoft.keepass.database.action.node.MoveEntryRunnable +import com.kunzisoft.keepass.database.action.node.MoveGroupRunnable +import com.kunzisoft.keepass.database.action.node.UpdateGroupRunnable +import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment +import com.kunzisoft.keepass.dialogs.GroupEditDialogFragment +import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment +import com.kunzisoft.keepass.dialogs.PasswordEncodingDialogHelper +import com.kunzisoft.keepass.dialogs.ReadOnlyDialog +import com.kunzisoft.keepass.dialogs.SortDialogFragment +import com.kunzisoft.keepass.education.GroupActivityEducation +import com.kunzisoft.keepass.magikeyboard.KeyboardEntryNotificationService +import com.kunzisoft.keepass.magikeyboard.KeyboardHelper +import com.kunzisoft.keepass.magikeyboard.MagikIME +import com.kunzisoft.keepass.model.Entry +import com.kunzisoft.keepass.model.Field +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.timeout.TimeoutHelper +import com.kunzisoft.keepass.utils.MenuUtil +import com.kunzisoft.keepass.view.AddNodeButtonView + +import net.cachapa.expandablelayout.ExpandableLayout + +class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListener, IconPickerDialogFragment.IconPickerListener, NodeAdapter.NodeMenuListener, ListNodesFragment.OnScrollListener, AssignMasterKeyDialogFragment.AssignPasswordDialogListener, NodeAdapter.NodeClickCallback, SortDialogFragment.SortSelectionListener { + + // Views + private var toolbar: Toolbar? = null + private var searchTitleView: View? = null + private var toolbarPasteExpandableLayout: ExpandableLayout? = null + private var toolbarPaste: Toolbar? = null + private var iconView: ImageView? = null + private var modeTitleView: TextView? = null + private var addNodeButtonView: AddNodeButtonView? = null + private var groupNameView: TextView? = null + + private var mDatabase: Database? = null + + private var listNodesFragment: ListNodesFragment? = null + private var currentGroupIsASearch: Boolean = false + + // Nodes + private var mRootGroup: GroupVersioned? = null + private var mCurrentGroup: GroupVersioned? = null + private var mOldGroupToUpdate: GroupVersioned? = null + private var nodeToCopy: NodeVersioned? = null + private var nodeToMove: NodeVersioned? = null + + private var searchSuggestionAdapter: SearchEntryCursorAdapter? = null + + private var iconColor: Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (isFinishing) { + return + } + mDatabase = App.currentDatabase + + // Construct main view + setContentView(layoutInflater.inflate(R.layout.list_nodes_with_add_button, null)) + + // Initialize views + iconView = findViewById(R.id.icon) + addNodeButtonView = findViewById(R.id.add_node_button) + toolbar = findViewById(R.id.toolbar) + searchTitleView = findViewById(R.id.search_title) + groupNameView = findViewById(R.id.group_name) + toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout) + toolbarPaste = findViewById(R.id.toolbar_paste) + modeTitleView = findViewById(R.id.mode_title_view) + + // Focus view to reinitialize timeout + resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView) + + // Retrieve elements after an orientation change + if (savedInstanceState != null) { + if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY)) + mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY) + if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) { + nodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY) + toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener()) + } else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) { + nodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY) + toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener()) + } + } + + try { + mRootGroup = mDatabase?.rootGroup + } catch (e: NullPointerException) { + Log.e(TAG, "Unable to get rootGroup") + } + + mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState) + currentGroupIsASearch = Intent.ACTION_SEARCH == intent.action + + Log.i(TAG, "Started creating tree") + if (mCurrentGroup == null) { + Log.w(TAG, "Group was null") + return + } + + // Update last access time. + mCurrentGroup?.touch(modified = false, touchParents = false) + + toolbar?.title = "" + setSupportActionBar(toolbar) + + toolbarPaste?.inflateMenu(R.menu.node_paste_menu) + toolbarPaste?.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp) + toolbarPaste?.setNavigationOnClickListener { + toolbarPasteExpandableLayout?.collapse() + nodeToCopy = null + nodeToMove = null + } + + // Retrieve the textColor to tint the icon + iconColor = theme + .obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) + .getColor(0, Color.WHITE) + + var fragmentTag = LIST_NODES_FRAGMENT_TAG + if (currentGroupIsASearch) + fragmentTag = SEARCH_FRAGMENT_TAG + + // Initialize the fragment with the list + listNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment? + if (listNodesFragment == null) + listNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, readOnly, currentGroupIsASearch) + + // Attach fragment to content view + supportFragmentManager.beginTransaction().replace( + R.id.nodes_list_fragment_container, + listNodesFragment, + fragmentTag) + .commit() + + // Add listeners to the add buttons + addNodeButtonView?.setAddGroupClickListener { + GroupEditDialogFragment.build() + .show(supportFragmentManager, + GroupEditDialogFragment.TAG_CREATE_GROUP) + } + mCurrentGroup?.let { currentGroup -> + addNodeButtonView?.setAddEntryClickListener { + EntryEditActivity.launch(this@GroupActivity, currentGroup) + } + } + + // Search suggestion + searchSuggestionAdapter = SearchEntryCursorAdapter(this, mDatabase) + + Log.i(TAG, "Finished creating tree") + } + + override fun onNewIntent(intent: Intent) { + Log.d(TAG, "setNewIntent: $intent") + setIntent(intent) + currentGroupIsASearch = if (Intent.ACTION_SEARCH == intent.action) { + // only one instance of search in backstack + openSearchGroup(retrieveCurrentGroup(intent, null)) + true + } else { + false + } + } + + private fun openSearchGroup(group: GroupVersioned?) { + // Delete the previous search fragment + val searchFragment = supportFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG) + if (searchFragment != null) { + if (supportFragmentManager + .popBackStackImmediate(SEARCH_FRAGMENT_TAG, + FragmentManager.POP_BACK_STACK_INCLUSIVE)) + supportFragmentManager.beginTransaction().remove(searchFragment).commit() + } + openGroup(group, true) + } + + private fun openChildGroup(group: GroupVersioned) { + openGroup(group, false) + } + + private fun openGroup(group: GroupVersioned?, isASearch: Boolean) { + // Check TimeoutHelper + TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) { + // Open a group in a new fragment + val newListNodeFragment = ListNodesFragment.newInstance(group, readOnly, isASearch) + val fragmentTransaction = supportFragmentManager.beginTransaction() + // Different animation + val fragmentTag: String + fragmentTag = if (isASearch) { + fragmentTransaction.setCustomAnimations(R.anim.slide_in_top, R.anim.slide_out_bottom, + R.anim.slide_in_bottom, R.anim.slide_out_top) + SEARCH_FRAGMENT_TAG + } else { + fragmentTransaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, + R.anim.slide_in_left, R.anim.slide_out_right) + LIST_NODES_FRAGMENT_TAG + } + + fragmentTransaction.replace(R.id.nodes_list_fragment_container, + newListNodeFragment, + fragmentTag) + fragmentTransaction.addToBackStack(fragmentTag) + fragmentTransaction.commit() + + listNodesFragment = newListNodeFragment + mCurrentGroup = group + assignGroupViewElements() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + mCurrentGroup?.let { + outState.putParcelable(GROUP_ID_KEY, it.nodeId) + } + mOldGroupToUpdate?.let { + outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it) + } + nodeToCopy?.let { + outState.putParcelable(NODE_TO_COPY_KEY, it) + } + nodeToMove?.let { + outState.putParcelable(NODE_TO_MOVE_KEY, it) + } + super.onSaveInstanceState(outState) + } + + private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): GroupVersioned? { + + // If it's a search + if (Intent.ACTION_SEARCH == intent.action) { + return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' }) + } + // else a real group + else { + var pwGroupId: PwNodeId<*>? = null + if (savedInstanceState != null && savedInstanceState.containsKey(GROUP_ID_KEY)) { + pwGroupId = savedInstanceState.getParcelable(GROUP_ID_KEY) + } else { + if (getIntent() != null) + 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) { + mRootGroup + } else { + mDatabase?.getGroupById(pwGroupId) + } + + return currentGroup + } + } + + private fun assignGroupViewElements() { + // Assign title + if (mCurrentGroup != null) { + val title = mCurrentGroup?.title + if (title != null && title.isNotEmpty()) { + if (groupNameView != null) { + groupNameView?.text = title + groupNameView?.invalidate() + } + } else { + if (groupNameView != null) { + groupNameView?.text = getText(R.string.root) + groupNameView?.invalidate() + } + } + } + if (currentGroupIsASearch) { + searchTitleView?.visibility = View.VISIBLE + } else { + searchTitleView?.visibility = View.GONE + } + + // Assign icon + if (currentGroupIsASearch) { + if (toolbar != null) { + toolbar?.navigationIcon = null + } + iconView?.visibility = View.GONE + } else { + // Assign the group icon depending of IconPack or custom icon + iconView?.visibility = View.VISIBLE + mCurrentGroup?.let { + mDatabase?.drawFactory?.assignDatabaseIconTo(this, iconView, it.icon, iconColor) + + if (toolbar != null) { + if (mCurrentGroup?.containsParent() == true) + toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp) + else { + toolbar?.navigationIcon = null + } + } + } + } + + // Show selection mode message if needed + if (selectionMode) { + modeTitleView?.visibility = View.VISIBLE + } else { + modeTitleView?.visibility = View.GONE + } + + // Show button if allowed + addNodeButtonView?.apply { + + // To enable add button + val addGroupEnabled = !readOnly && !currentGroupIsASearch + var addEntryEnabled = !readOnly && !currentGroupIsASearch + mCurrentGroup?.let { + val isRoot = it == mRootGroup + if (!it.allowAddEntryIfIsRoot()) + addEntryEnabled = !isRoot && addEntryEnabled + if (isRoot) { + showWarnings() + } + } + enableAddGroup(addGroupEnabled) + enableAddEntry(addEntryEnabled) + + if (isEnable) + showButton() + } + } + + override fun onScrolled(dy: Int) { + addNodeButtonView?.hideButtonOnScrollListener(dy) + } + + override fun onNodeClick(node: NodeVersioned) { + when (node.type) { + Type.GROUP -> try { + openChildGroup(node as GroupVersioned) + } catch (e: ClassCastException) { + Log.e(TAG, "Node can't be cast in Group") + } + + Type.ENTRY -> try { + val entry = node as EntryVersioned + EntrySelectionHelper.doEntrySelectionAction(intent, + { + EntryActivity.launch(this@GroupActivity, entry, readOnly) + }, + { + MagikIME.setEntryKey(getEntry(entry)) + // Show the notification if allowed in Preferences + if (PreferencesUtil.enableKeyboardNotificationEntry(this@GroupActivity)) { + startService(Intent( + this@GroupActivity, + KeyboardEntryNotificationService::class.java)) + } + // Consume the selection mode + EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent) + moveTaskToBack(true) + }, + { + // Build response with the entry selected + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity, entry) + } + finish() + }) + } catch (e: ClassCastException) { + Log.e(TAG, "Node can't be cast in Entry") + } + } + } + + // TODO Builder + private fun getEntry(entry: EntryVersioned): Entry { + val entryModel = Entry() + entryModel.title = entry.title + entryModel.username = entry.username + entryModel.password = entry.password + entryModel.url = entry.url + if (entry.containsCustomFields()) { + entry.fields + .doActionToAllCustomProtectedField { key, value -> + entryModel.addCustomField( + Field(key, value.toString())) + } + } + return entryModel + } + + override fun onOpenMenuClick(node: NodeVersioned): Boolean { + onNodeClick(node) + return true + } + + override fun onEditMenuClick(node: NodeVersioned): Boolean { + when (node.type) { + Type.GROUP -> { + mOldGroupToUpdate = node as GroupVersioned + GroupEditDialogFragment.build(mOldGroupToUpdate!!) + .show(supportFragmentManager, + GroupEditDialogFragment.TAG_CREATE_GROUP) + } + Type.ENTRY -> EntryEditActivity.launch(this@GroupActivity, node as EntryVersioned) + } + return true + } + + override fun onCopyMenuClick(node: NodeVersioned): Boolean { + toolbarPasteExpandableLayout?.expand() + nodeToCopy = node + toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener()) + return false + } + + private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener { + override fun onMenuItemClick(item: MenuItem): Boolean { + toolbarPasteExpandableLayout?.collapse() + + when (item.itemId) { + R.id.menu_paste -> { + when (nodeToCopy?.type) { + Type.GROUP -> Log.e(TAG, "Copy not allowed for group") + Type.ENTRY -> { + mCurrentGroup?.let { currentGroup -> + copyEntry(nodeToCopy as EntryVersioned, currentGroup) + } + } + } + nodeToCopy = null + return true + } + } + return true + } + } + + private fun copyEntry(entryToCopy: EntryVersioned, newParent: GroupVersioned) { + Thread(CopyEntryRunnable(this, + App.currentDatabase, + entryToCopy, + newParent, + AfterAddNodeRunnable(), + !readOnly) + ).start() + } + + override fun onMoveMenuClick(node: NodeVersioned): Boolean { + toolbarPasteExpandableLayout?.expand() + nodeToMove = node + toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener()) + return false + } + + private inner class OnMoveMenuItemClickListener : Toolbar.OnMenuItemClickListener { + override fun onMenuItemClick(item: MenuItem): Boolean { + toolbarPasteExpandableLayout?.collapse() + + when (item.itemId) { + R.id.menu_paste -> { + when (nodeToMove?.type) { + Type.GROUP -> { + mCurrentGroup?.let { currentGroup -> + moveGroup(nodeToMove as GroupVersioned, currentGroup) + } + } + Type.ENTRY -> { + mCurrentGroup?.let { currentGroup -> + moveEntry(nodeToMove as EntryVersioned, currentGroup) + } + } + } + nodeToMove = null + return true + } + } + return true + } + } + + private fun moveGroup(groupToMove: GroupVersioned, newParent: GroupVersioned) { + Thread(MoveGroupRunnable( + this, + App.currentDatabase, + groupToMove, + newParent, + AfterAddNodeRunnable(), + !readOnly) + ).start() + } + + private fun moveEntry(entryToMove: EntryVersioned, newParent: GroupVersioned) { + Thread(MoveEntryRunnable( + this, + App.currentDatabase, + entryToMove, + newParent, + AfterAddNodeRunnable(), + !readOnly) + ).start() + } + + override fun onDeleteMenuClick(node: NodeVersioned): Boolean { + when (node.type) { + Type.GROUP -> deleteGroup(node as GroupVersioned) + Type.ENTRY -> deleteEntry(node as EntryVersioned) + } + return true + } + + private fun deleteGroup(group: GroupVersioned) { + //TODO Verify trash recycle bin + Thread(DeleteGroupRunnable( + this, + App.currentDatabase, + group, + AfterDeleteNodeRunnable(), + !readOnly) + ).start() + } + + private fun deleteEntry(entry: EntryVersioned) { + Thread(DeleteEntryRunnable( + this, + App.currentDatabase, + entry, + AfterDeleteNodeRunnable(), + !readOnly) + ).start() + } + + override fun onResume() { + super.onResume() + // Refresh the elements + assignGroupViewElements() + // Refresh suggestions to change preferences + searchSuggestionAdapter?.reInit(this) + } + + override fun onStop() { + super.onStop() + // Hide button + addNodeButtonView?.hideButton() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + + val inflater = menuInflater + inflater.inflate(R.menu.search, menu) + inflater.inflate(R.menu.database_lock, menu) + if (!readOnly) + inflater.inflate(R.menu.database_master_key, menu) + if (!selectionMode) { + inflater.inflate(R.menu.default_menu, menu) + MenuUtil.contributionMenuInflater(inflater, menu) + } + + // Get the SearchView and set the searchable configuration + val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager + + menu.findItem(R.id.menu_search)?.let { + val searchView = it.actionView as SearchView? + searchView?.apply { + setSearchableInfo(searchManager.getSearchableInfo( + ComponentName(this@GroupActivity, GroupActivity::class.java))) + setIconifiedByDefault(false) // Do not iconify the widget; expand it by default + suggestionsAdapter = searchSuggestionAdapter + setOnSuggestionListener(object : SearchView.OnSuggestionListener { + override fun onSuggestionClick(position: Int): Boolean { + searchSuggestionAdapter?.let { searchAdapter -> + onNodeClick(searchAdapter.getEntryFromPosition(position)) + } + return true + } + + override fun onSuggestionSelect(position: Int): Boolean { + return true + } + }) + } + } + + super.onCreateOptionsMenu(menu) + + // Launch education screen + Handler().post { performedNextEducation(GroupActivityEducation(this), menu) } + + return true + } + + private fun performedNextEducation(groupActivityEducation: GroupActivityEducation, + menu: Menu) { + // If no node, show education to add new one + if (listNodesFragment != null + && listNodesFragment!!.isEmpty + && addNodeButtonView != null + && addNodeButtonView!!.isEnable + && groupActivityEducation.checkAndPerformedAddNodeButtonEducation( + addNodeButtonView!!, + { + addNodeButtonView?.openButtonIfClose() + }, + { + 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( + 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) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + R.id.menu_search -> + //onSearchRequested(); + return true + R.id.menu_lock -> { + lockAndExit() + return true + } + R.id.menu_change_master_key -> { + setPassword() + return true + } + else -> { + // Check the time lock before launching settings + MenuUtil.onDefaultMenuOptionsItemSelected(this, item, readOnly, true) + return super.onOptionsItemSelected(item) + } + } + } + + private fun setPassword() { + AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog") + } + + override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction, + name: String, + icon: PwIcon) { + val database = App.currentDatabase + + when (action) { + GroupEditDialogFragment.EditGroupDialogAction.CREATION -> { + // If group creation + mCurrentGroup?.let { currentGroup -> + // Build the group + database.createGroup()?.let { newGroup-> + newGroup.title = name + newGroup.icon = icon + // Not really needed here because added in runnable but safe + newGroup.parent = currentGroup + + // If group created save it in the database + Thread(AddGroupRunnable(this, + App.currentDatabase, + newGroup, + currentGroup, + AfterAddNodeRunnable(), + !readOnly) + ).start() + } + } + } + GroupEditDialogFragment.EditGroupDialogAction.UPDATE -> + // If update add new elements + mOldGroupToUpdate?.let { oldGroupToUpdate -> + GroupVersioned(oldGroupToUpdate).let { updateGroup -> + updateGroup.title = name + // TODO custom icon + updateGroup.icon = icon + + listNodesFragment?.removeNode(oldGroupToUpdate) + + // If group updated save it in the database + Thread(UpdateGroupRunnable(this, + App.currentDatabase, + oldGroupToUpdate, + updateGroup, + AfterUpdateNodeRunnable(), + !readOnly) + ).start() + } + } + else -> {} + } + } + + internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() { + override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { + runOnUiThread { + if (actionNodeValues.success) { + listNodesFragment?.addNode(actionNodeValues.newNode) + } + } + } + } + + internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() { + override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { + runOnUiThread { + if (actionNodeValues.success) { + listNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode) + } + } + } + } + + internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() { + override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { + runOnUiThread { + if (actionNodeValues.success) { + listNodesFragment?.removeNode(actionNodeValues.oldNode) + + actionNodeValues.oldNode?.let { oldNode -> + val parent = oldNode.parent + val database = App.currentDatabase + if (database.isRecycleBinAvailable && database.isRecycleBinEnabled) { + val recycleBin = database.recycleBin + // Add trash if it doesn't exists + if (parent == recycleBin + && mCurrentGroup != null + && mCurrentGroup!!.parent == null + && mCurrentGroup != recycleBin) { + listNodesFragment?.addNode(parent) + } + } + } + } + } + } + } + + override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction, + name: String, + iconId: PwIcon) { + // Do nothing here + } + + override// For icon in create tree dialog + fun iconPicked(bundle: Bundle) { + (supportFragmentManager + .findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment) + .iconPicked(bundle) + } + + private fun showWarnings() { + // TODO Preferences + if (App.currentDatabase.isReadOnly) { + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + if (prefs.getBoolean(getString(R.string.show_read_only_warning), true)) { + ReadOnlyDialog(this).show() + } + } + } + + override fun onAssignKeyDialogPositiveClick( + masterPasswordChecked: Boolean, masterPassword: String?, + keyFileChecked: Boolean, keyFile: Uri?) { + + mDatabase?.let { database -> + val taskThread = Thread(ProgressDialogRunnable(this, + R.string.saving_database + ) { + AssignPasswordInDatabaseRunnable(this@GroupActivity, + database, + masterPasswordChecked, + masterPassword, + keyFileChecked, + keyFile, + true) // TODO save + }) + // Show the progress dialog now or after dialog confirmation + if (database.validatePasswordEncoding(masterPassword!!)) { + taskThread.start() + } else { + PasswordEncodingDialogHelper() + .show(this, DialogInterface.OnClickListener{ _, _ -> taskThread.start() }) + } + } + } + + override fun onAssignKeyDialogNegativeClick( + masterPasswordChecked: Boolean, masterPassword: String?, + keyFileChecked: Boolean, keyFile: Uri?) { + + } + + override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) { + listNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom) + } + + 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 + // Is refresh from onResume() + } + + @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 + * another app such as Files or GoogleDrive and then Search for an entry. Here we remove the + * FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task. + */ + if (Intent.ACTION_SEARCH == intent.action) { + var flags = intent.flags + flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv() + intent.flags = flags + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + super.startActivityForResult(intent, requestCode, options) + } + } + + private fun removeSearchInIntent(intent: Intent) { + if (Intent.ACTION_SEARCH == intent.action) { + currentGroupIsASearch = false + intent.action = Intent.ACTION_DEFAULT + intent.removeExtra(SearchManager.QUERY) + } + } + + override fun onBackPressed() { + + // Normal way when we are not in root + if (mRootGroup != null && mRootGroup != mCurrentGroup) + super.onBackPressed() + // Else lock if needed + else { + if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) { + App.currentDatabase.closeAndClear(applicationContext) + super.onBackPressed() + } else { + moveTaskToBack(true) + } + } + + listNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment + // to refresh fragment + listNodesFragment?.rebuildList() + mCurrentGroup = listNodesFragment?.mainGroup + removeSearchInIntent(intent) + assignGroupViewElements() + } + + companion object { + + private val TAG = GroupActivity::class.java.name + + private const val GROUP_ID_KEY = "GROUP_ID_KEY" + private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG" + private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG" + private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY" + private const val NODE_TO_COPY_KEY = "NODE_TO_COPY_KEY" + private const val NODE_TO_MOVE_KEY = "NODE_TO_MOVE_KEY" + + private fun buildAndLaunchIntent(activity: Activity, group: GroupVersioned?, readOnly: Boolean, + intentBuildLauncher: (Intent) -> Unit) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { + val intent = Intent(activity, GroupActivity::class.java) + if (group != null) { + intent.putExtra(GROUP_ID_KEY, group.nodeId) + } + ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly) + intentBuildLauncher.invoke(intent) + } + } + + /* + * ------------------------- + * Standard Launch + * ------------------------- + */ + + @JvmOverloads + fun launch(activity: Activity, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) { + launch(activity, null, readOnly) + } + + fun launch(activity: Activity, group: GroupVersioned?, readOnly: Boolean) { + TimeoutHelper.recordTime(activity) + buildAndLaunchIntent(activity, group, readOnly) { intent -> + activity.startActivityForResult(intent, 0) + } + } + + /* + * ------------------------- + * Keyboard Launch + * ------------------------- + */ + // TODO implement pre search to directly open the direct group + + fun launchForKeyboardSelection(activity: Activity, readOnly: Boolean) { + TimeoutHelper.recordTime(activity) + buildAndLaunchIntent(activity, null, readOnly) { intent -> + KeyboardHelper.startActivityForKeyboardSelection(activity, intent) + } + } + + /* + * ------------------------- + * Autofill Launch + * ------------------------- + */ + // TODO implement pre search to directly open the direct group + + @RequiresApi(api = Build.VERSION_CODES.O) + fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure, readOnly: Boolean) { + TimeoutHelper.recordTime(activity) + buildAndLaunchIntent(activity, null, readOnly) { intent -> + AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure) + } + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt index 90f0d3326..c15a114d5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt @@ -24,7 +24,6 @@ import android.net.Uri import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.exception.InvalidKeyFileException import com.kunzisoft.keepass.tasks.ActionRunnable -import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.getUriInputStream import java.io.IOException @@ -35,8 +34,8 @@ class AssignPasswordInDatabaseRunnable @JvmOverloads constructor( masterPassword: String?, withKeyFile: Boolean, keyFile: Uri?, - actionRunnable: ActionRunnable? = null, - save: Boolean) + save: Boolean, + actionRunnable: ActionRunnable? = null) : SaveDatabaseRunnable(ctx, db, actionRunnable, save) { private var mMasterPassword: String? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java index 08007f6ef..345a357dd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java @@ -468,8 +468,8 @@ public class FileDatabaseSelectActivity extends StylishActivity implements masterPassword, keyFileChecked, keyFile, - new LaunchGroupActivityFinish(UriUtil.parseDefaultFile(databaseFilename)), - true // TODO get readonly + true, // TODO get readonly + new LaunchGroupActivityFinish(UriUtil.parseDefaultFile(databaseFilename)) ); }) )).start(); @@ -505,7 +505,7 @@ public class FileDatabaseSelectActivity extends StylishActivity implements fileDatabaseHistory.addDatabaseUri(fileURI); mAdapter.notifyDataSetChanged(); updateFileListVisibility(); - GroupActivity.launch(FileDatabaseSelectActivity.this); + GroupActivity.Companion.launch(FileDatabaseSelectActivity.this); } else { Log.e(TAG, "Unable to open the database"); } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/IntentBuildLauncher.java b/app/src/main/java/com/kunzisoft/keepass/password/IntentBuildLauncher.java similarity index 73% rename from app/src/main/java/com/kunzisoft/keepass/activities/IntentBuildLauncher.java rename to app/src/main/java/com/kunzisoft/keepass/password/IntentBuildLauncher.java index 0d3e0e4be..aa69659d1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/IntentBuildLauncher.java +++ b/app/src/main/java/com/kunzisoft/keepass/password/IntentBuildLauncher.java @@ -1,4 +1,4 @@ -package com.kunzisoft.keepass.activities; +package com.kunzisoft.keepass.password; import android.content.Intent; diff --git a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java index 685898e4f..5b7a941c3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java @@ -52,7 +52,6 @@ import android.widget.Toast; import com.kunzisoft.keepass.R; import com.kunzisoft.keepass.activities.EntrySelectionHelper; import com.kunzisoft.keepass.activities.GroupActivity; -import com.kunzisoft.keepass.activities.IntentBuildLauncher; import com.kunzisoft.keepass.activities.ReadOnlyHelper; import com.kunzisoft.keepass.activities.lock.LockingActivity; import com.kunzisoft.keepass.app.App; @@ -826,18 +825,18 @@ public class PasswordActivity extends StylishActivity private void launchGroupActivity() { EntrySelectionHelper.INSTANCE.doEntrySelectionAction(getIntent(), () -> { - GroupActivity.launch(PasswordActivity.this, readOnly); + GroupActivity.Companion.launch(PasswordActivity.this, readOnly); return null; }, () -> { - GroupActivity.launchForKeyboardSelection(PasswordActivity.this, readOnly); + GroupActivity.Companion.launchForKeyboardSelection(PasswordActivity.this, readOnly); // Do not keep history finish(); return null; }, assistStructure -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - GroupActivity.launchForAutofillResult(PasswordActivity.this, assistStructure, readOnly); + GroupActivity.Companion.launchForAutofillResult(PasswordActivity.this, assistStructure, readOnly); } return null; }); From cd271e6f04e8e7010f5112377fbcd96c77f407d4 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 7 Jul 2019 20:22:06 +0200 Subject: [PATCH 11/24] Kotlinized ListNodesFragment --- .../keepass/activities/GroupActivity.kt | 30 +- .../keepass/activities/ListNodesFragment.java | 294 ------------------ .../keepass/activities/ListNodesFragment.kt | 277 +++++++++++++++++ 3 files changed, 294 insertions(+), 307 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt 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 55bc7e07d..8f229a27e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -818,7 +818,8 @@ class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListen override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { runOnUiThread { if (actionNodeValues.success) { - listNodesFragment?.addNode(actionNodeValues.newNode) + if (actionNodeValues.newNode != null) + listNodesFragment?.addNode(actionNodeValues.newNode) } } } @@ -828,7 +829,8 @@ class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListen override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { runOnUiThread { if (actionNodeValues.success) { - listNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode) + if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null) + listNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode) } } } @@ -838,19 +840,21 @@ class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListen override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { runOnUiThread { if (actionNodeValues.success) { - listNodesFragment?.removeNode(actionNodeValues.oldNode) + if (actionNodeValues.oldNode != null) + listNodesFragment?.removeNode(actionNodeValues.oldNode) actionNodeValues.oldNode?.let { oldNode -> - val parent = oldNode.parent - val database = App.currentDatabase - if (database.isRecycleBinAvailable && database.isRecycleBinEnabled) { - val recycleBin = database.recycleBin - // Add trash if it doesn't exists - if (parent == recycleBin - && mCurrentGroup != null - && mCurrentGroup!!.parent == null - && mCurrentGroup != recycleBin) { - listNodesFragment?.addNode(parent) + oldNode.parent?.let { parent -> + val database = App.currentDatabase + if (database.isRecycleBinAvailable && database.isRecycleBinEnabled) { + val recycleBin = database.recycleBin + // Add trash if it doesn't exists + if (parent == recycleBin + && mCurrentGroup != null + && mCurrentGroup!!.parent == null + && mCurrentGroup != recycleBin) { + listNodesFragment?.addNode(parent) + } } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.java b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.java deleted file mode 100644 index 9d82dcc76..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.java +++ /dev/null @@ -1,294 +0,0 @@ -package com.kunzisoft.keepass.activities; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.adapters.NodeAdapter; -import com.kunzisoft.keepass.database.SortNodeEnum; -import com.kunzisoft.keepass.database.element.GroupVersioned; -import com.kunzisoft.keepass.database.element.NodeVersioned; -import com.kunzisoft.keepass.dialogs.SortDialogFragment; -import com.kunzisoft.keepass.settings.PreferencesUtil; -import com.kunzisoft.keepass.stylish.StylishFragment; - -public class ListNodesFragment extends StylishFragment implements - SortDialogFragment.SortSelectionListener { - - private static final String TAG = ListNodesFragment.class.getName(); - - private static final String GROUP_KEY = "GROUP_KEY"; - private static final String IS_SEARCH = "IS_SEARCH"; - - private NodeAdapter.NodeClickCallback nodeClickCallback; - private NodeAdapter.NodeMenuListener nodeMenuListener; - private OnScrollListener onScrollListener; - - private RecyclerView listView; - private GroupVersioned currentGroup; - private NodeAdapter mAdapter; - - private View notFoundView; - private boolean isASearchResult; - - // Preferences for sorting - private SharedPreferences prefs; - - private boolean readOnly; - - public static ListNodesFragment newInstance(GroupVersioned group, boolean readOnly, boolean isASearch) { - Bundle bundle = new Bundle(); - if (group != null) { - bundle.putParcelable(GROUP_KEY, group); - } - bundle.putBoolean(IS_SEARCH, isASearch); - ReadOnlyHelper.INSTANCE.putReadOnlyInBundle(bundle, readOnly); - ListNodesFragment listNodesFragment = new ListNodesFragment(); - listNodesFragment.setArguments(bundle); - return listNodesFragment; - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - try { - nodeClickCallback = (NodeAdapter.NodeClickCallback) context; - } catch (ClassCastException e) { - // The activity doesn't implement the interface, throw exception - throw new ClassCastException(context.toString() - + " must implement " + NodeAdapter.NodeClickCallback.class.getName()); - } - try { - nodeMenuListener = (NodeAdapter.NodeMenuListener) context; - } catch (ClassCastException e) { - nodeMenuListener = null; - // Context menu can be omit - Log.w(TAG, context.toString() - + " must implement " + NodeAdapter.NodeMenuListener.class.getName()); - } - try { - onScrollListener = (OnScrollListener) context; - } catch (ClassCastException e) { - onScrollListener = null; - // Context menu can be omit - Log.w(TAG, context.toString() - + " must implement " + RecyclerView.OnScrollListener.class.getName()); - } - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if ( getActivity() != null ) { - setHasOptionsMenu(true); - - readOnly = ReadOnlyHelper.INSTANCE.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, getArguments()); - - if (getArguments() != null) { - // Contains all the group in element - if (getArguments().containsKey(GROUP_KEY)) { - currentGroup = getArguments().getParcelable(GROUP_KEY); - } - - if (getArguments().containsKey(IS_SEARCH)) { - isASearchResult = getArguments().getBoolean(IS_SEARCH); - } - } - - mAdapter = new NodeAdapter(getContextThemed(), getActivity().getMenuInflater()); - mAdapter.setReadOnly(readOnly); - mAdapter.setIsASearchResult(isASearchResult); - mAdapter.setOnNodeClickListener(nodeClickCallback); - - if (nodeMenuListener != null) { - mAdapter.setActivateContextMenu(true); - mAdapter.setNodeMenuListener(nodeMenuListener); - } - prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - ReadOnlyHelper.INSTANCE.onSaveInstanceState(outState, readOnly); - super.onSaveInstanceState(outState); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - - // To apply theme - View rootView = inflater.cloneInContext(getContextThemed()) - .inflate(R.layout.list_nodes_fragment, container, false); - listView = rootView.findViewById(R.id.nodes_list); - notFoundView = rootView.findViewById(R.id.not_found_container); - - if (onScrollListener != null) { - listView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); - onScrollListener.onScrolled(dy); - } - }); - } - - return rootView; - } - - @Override - public void onResume() { - super.onResume(); - - rebuildList(); - - if (isASearchResult && mAdapter.isEmpty()) { - // To show the " no search entry found " - listView.setVisibility(View.GONE); - notFoundView.setVisibility(View.VISIBLE); - } else { - listView.setVisibility(View.VISIBLE); - notFoundView.setVisibility(View.GONE); - } - } - - public void rebuildList() { - // Add elements to the list - if (currentGroup != null) - mAdapter.rebuildList(currentGroup); - assignListToNodeAdapter(listView); - } - - protected void assignListToNodeAdapter(RecyclerView recyclerView) { - recyclerView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(mAdapter); - } - - @Override - public void onSortSelected(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore, boolean recycleBinBottom) { - // Toggle setting - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(getString(R.string.sort_node_key), sortNodeEnum.name()); - editor.putBoolean(getString(R.string.sort_ascending_key), ascending); - editor.putBoolean(getString(R.string.sort_group_before_key), groupsBefore); - editor.putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom); - editor.apply(); - - // Tell the adapter to refresh it's list - mAdapter.notifyChangeSort(sortNodeEnum, ascending, groupsBefore); - mAdapter.rebuildList(currentGroup); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.tree, menu); - - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch ( item.getItemId() ) { - - case R.id.menu_sort: - SortDialogFragment sortDialogFragment; - - /* - // TODO Recycle bin bottom - if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) { - sortDialogFragment = - SortDialogFragment.getInstance( - PrefsUtil.getListSort(this), - PrefsUtil.getAscendingSort(this), - PrefsUtil.getGroupsBeforeSort(this), - PrefsUtil.getRecycleBinBottomSort(this)); - } else { - */ - sortDialogFragment = - SortDialogFragment.getInstance( - PreferencesUtil.getListSort(getContext()), - PreferencesUtil.getAscendingSort(getContext()), - PreferencesUtil.getGroupsBeforeSort(getContext())); - //} - - sortDialogFragment.show(getChildFragmentManager(), "sortDialog"); - return true; - - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - switch (requestCode) { - case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE: - if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE || - resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) { - NodeVersioned newNode = data.getParcelableExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY); - if (newNode != null) { - if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE) - mAdapter.addNode(newNode); - if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) { - //mAdapter.updateLastNodeRegister(newNode); - mAdapter.rebuildList(currentGroup); - } - } else { - Log.e(this.getClass().getName(), "New node can be retrieve in Activity Result"); - } - } - break; - } - } - - public boolean isEmpty() { - return mAdapter == null || mAdapter.getItemCount() <= 0; - } - - public void addNode(NodeVersioned newNode) { - mAdapter.addNode(newNode); - } - - public void updateNode(NodeVersioned oldNode, NodeVersioned newNode) { - mAdapter.updateNode(oldNode, newNode); - } - - public void removeNode(NodeVersioned pwNode) { - mAdapter.removeNode(pwNode); - } - - public GroupVersioned getMainGroup() { - return currentGroup; - } - - public interface OnScrollListener { - - /** - * Callback method to be invoked when the RecyclerView has been scrolled. This will be - * called after the scroll has completed. - * - * @param dy The amount of vertical scroll. - */ - void onScrolled(int dy); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt new file mode 100644 index 000000000..465c11c04 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt @@ -0,0 +1,277 @@ +package com.kunzisoft.keepass.activities + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.preference.PreferenceManager +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup + +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.adapters.NodeAdapter +import com.kunzisoft.keepass.database.SortNodeEnum +import com.kunzisoft.keepass.database.element.GroupVersioned +import com.kunzisoft.keepass.database.element.NodeVersioned +import com.kunzisoft.keepass.dialogs.SortDialogFragment +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.stylish.StylishFragment + +class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener { + + private var nodeClickCallback: NodeAdapter.NodeClickCallback? = null + private var nodeMenuListener: NodeAdapter.NodeMenuListener? = null + private var onScrollListener: OnScrollListener? = null + + private var listView: RecyclerView? = null + var mainGroup: GroupVersioned? = null + private set + private var mAdapter: NodeAdapter? = null + + private var notFoundView: View? = null + private var isASearchResult: Boolean = false + + // Preferences for sorting + private var prefs: SharedPreferences? = null + + private var readOnly: Boolean = false + + val isEmpty: Boolean + get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0 + + override fun onAttach(context: Context?) { + super.onAttach(context) + try { + nodeClickCallback = context as NodeAdapter.NodeClickCallback? + } catch (e: ClassCastException) { + // The activity doesn't implement the interface, throw exception + throw ClassCastException(context?.toString() + + " must implement " + NodeAdapter.NodeClickCallback::class.java.name) + } + + try { + nodeMenuListener = context as NodeAdapter.NodeMenuListener? + } catch (e: ClassCastException) { + nodeMenuListener = null + // Context menu can be omit + Log.w(TAG, context?.toString() + + " must implement " + NodeAdapter.NodeMenuListener::class.java.name) + } + + try { + onScrollListener = context as OnScrollListener? + } catch (e: ClassCastException) { + onScrollListener = null + // Context menu can be omit + Log.w(TAG, context?.toString() + + " must implement " + RecyclerView.OnScrollListener::class.java.name) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + activity?.let { currentActivity -> + setHasOptionsMenu(true) + + readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments) + + arguments?.let { args -> + // Contains all the group in element + if (args.containsKey(GROUP_KEY)) { + mainGroup = args.getParcelable(GROUP_KEY) + } + if (args.containsKey(IS_SEARCH)) { + isASearchResult = args.getBoolean(IS_SEARCH) + } + } + + mAdapter = NodeAdapter(getContextThemed(), currentActivity.menuInflater) + mAdapter?.apply { + setReadOnly(readOnly) + setIsASearchResult(isASearchResult) + setOnNodeClickListener(nodeClickCallback) + nodeMenuListener?.let { menuListener -> + setActivateContextMenu(true) + setNodeMenuListener(menuListener) + } + } + prefs = PreferenceManager.getDefaultSharedPreferences(context) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + ReadOnlyHelper.onSaveInstanceState(outState, readOnly) + super.onSaveInstanceState(outState) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + + // To apply theme + val rootView = inflater.cloneInContext(getContextThemed()) + .inflate(R.layout.list_nodes_fragment, container, false) + listView = rootView.findViewById(R.id.nodes_list) + notFoundView = rootView.findViewById(R.id.not_found_container) + + onScrollListener?.let { onScrollListener -> + listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + onScrollListener.onScrolled(dy) + } + }) + } + + return rootView + } + + override fun onResume() { + super.onResume() + + rebuildList() + + if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) { + // To show the " no search entry found " + listView?.visibility = View.GONE + notFoundView?.visibility = View.VISIBLE + } else { + listView?.visibility = View.VISIBLE + notFoundView?.visibility = View.GONE + } + } + + fun rebuildList() { + // Add elements to the list + mainGroup?.let { + mAdapter?.rebuildList(mainGroup) + } + listView?.apply { + scrollBarStyle = View.SCROLLBARS_INSIDE_INSET + layoutManager = LinearLayoutManager(context) + adapter = mAdapter + } + } + + override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) { + // Toggle setting + prefs?.edit()?.apply { + putString(getString(R.string.sort_node_key), sortNodeEnum.name) + putBoolean(getString(R.string.sort_ascending_key), ascending) + putBoolean(getString(R.string.sort_group_before_key), groupsBefore) + putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom) + apply() + } + + // Tell the adapter to refresh it's list + mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore) + mAdapter?.rebuildList(mainGroup) + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + inflater?.inflate(R.menu.tree, menu) + + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + + R.id.menu_sort -> { + val sortDialogFragment: SortDialogFragment + + /* + // TODO Recycle bin bottom + if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) { + sortDialogFragment = + SortDialogFragment.getInstance( + PrefsUtil.getListSort(this), + PrefsUtil.getAscendingSort(this), + PrefsUtil.getGroupsBeforeSort(this), + PrefsUtil.getRecycleBinBottomSort(this)); + } else { + */ + sortDialogFragment = SortDialogFragment.getInstance( + PreferencesUtil.getListSort(context), + PreferencesUtil.getAscendingSort(context), + PreferencesUtil.getGroupsBeforeSort(context)) + //} + + sortDialogFragment.show(childFragmentManager, "sortDialog") + return true + } + + else -> return super.onOptionsItemSelected(item) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + when (requestCode) { + EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> { + if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE + || resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) { + data?.getParcelableExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode -> + if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE) + mAdapter?.addNode(newNode) + if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) { + //mAdapter.updateLastNodeRegister(newNode); + mAdapter?.rebuildList(mainGroup) + } + } ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result") + } + } + } + } + + fun addNode(newNode: NodeVersioned) { + mAdapter?.addNode(newNode) + } + + fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) { + mAdapter?.updateNode(oldNode, newNode) + } + + fun removeNode(pwNode: NodeVersioned) { + mAdapter?.removeNode(pwNode) + } + + interface OnScrollListener { + + /** + * Callback method to be invoked when the RecyclerView has been scrolled. This will be + * called after the scroll has completed. + * + * @param dy The amount of vertical scroll. + */ + fun onScrolled(dy: Int) + } + + companion object { + + private val TAG = ListNodesFragment::class.java.name + + private const val GROUP_KEY = "GROUP_KEY" + private const val IS_SEARCH = "IS_SEARCH" + + fun newInstance(group: GroupVersioned?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment { + val bundle = Bundle() + if (group != null) { + bundle.putParcelable(GROUP_KEY, group) + } + bundle.putBoolean(IS_SEARCH, isASearch) + ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly) + val listNodesFragment = ListNodesFragment() + listNodesFragment.arguments = bundle + return listNodesFragment + } + } +} From b0bc0f9d856bd417e0f3c63925b0ecfc7ddf66af Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 7 Jul 2019 20:53:08 +0200 Subject: [PATCH 12/24] Kotlinized Adapters and fix StyledAttributes --- .../keepass/activities/EntryActivity.kt | 6 +- .../keepass/activities/EntryEditActivity.kt | 6 +- .../keepass/activities/GroupActivity.kt | 23 +- .../keepass/activities/ListNodesFragment.kt | 16 +- ...asicViewHolder.java => BasicViewHolder.kt} | 26 +- ...ntryViewHolder.java => EntryViewHolder.kt} | 21 +- ...roupViewHolder.java => GroupViewHolder.kt} | 21 +- .../keepass/adapters/NodeAdapter.java | 406 ------------------ .../kunzisoft/keepass/adapters/NodeAdapter.kt | 362 ++++++++++++++++ .../adapters/SearchEntryCursorAdapter.java | 138 ------ .../adapters/SearchEntryCursorAdapter.kt | 124 ++++++ .../dialogs/GroupEditDialogFragment.java | 1 + .../dialogs/IconPickerDialogFragment.java | 1 + .../keepass/view/EntryContentsView.kt | 2 +- 14 files changed, 552 insertions(+), 601 deletions(-) rename app/src/main/java/com/kunzisoft/keepass/adapters/{BasicViewHolder.java => BasicViewHolder.kt} (61%) rename app/src/main/java/com/kunzisoft/keepass/adapters/{EntryViewHolder.java => EntryViewHolder.kt} (59%) rename app/src/main/java/com/kunzisoft/keepass/adapters/{GroupViewHolder.java => GroupViewHolder.kt} (59%) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt delete mode 100644 app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt 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 04f346e0b..2d582cfe9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -99,9 +99,9 @@ class EntryActivity : LockingHideActivity() { mEntry?.touch(modified = false, touchParents = false) // Retrieve the textColor to tint the icon - iconColor = theme. - obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) - .getColor(0, Color.WHITE) + val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) + iconColor = taIconColor.getColor(0, Color.WHITE) + taIconColor.recycle() // Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set invalidateOptionsMenu() 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 4797af08b..92b042499 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -129,9 +129,9 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi mDatabase = App.currentDatabase // Retrieve the textColor to tint the icon - iconColor = theme - .obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) - .getColor(0, Color.WHITE) + val taIconColor = theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) + iconColor = taIconColor.getColor(0, Color.WHITE) + taIconColor.recycle() mSelectedIconStandard = mDatabase?.iconFactory?.unknownIcon 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 8f229a27e..117235011 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -81,7 +81,14 @@ import com.kunzisoft.keepass.view.AddNodeButtonView import net.cachapa.expandablelayout.ExpandableLayout -class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListener, IconPickerDialogFragment.IconPickerListener, NodeAdapter.NodeMenuListener, ListNodesFragment.OnScrollListener, AssignMasterKeyDialogFragment.AssignPasswordDialogListener, NodeAdapter.NodeClickCallback, SortDialogFragment.SortSelectionListener { +class GroupActivity : LockingActivity(), + GroupEditDialogFragment.EditGroupListener, + IconPickerDialogFragment.IconPickerListener, + NodeAdapter.NodeMenuListener, + ListNodesFragment.OnScrollListener, + AssignMasterKeyDialogFragment.AssignPasswordDialogListener, + NodeAdapter.NodeClickCallback, + SortDialogFragment.SortSelectionListener { // Views private var toolbar: Toolbar? = null @@ -176,9 +183,9 @@ class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListen } // Retrieve the textColor to tint the icon - iconColor = theme - .obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) - .getColor(0, Color.WHITE) + val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) + iconColor = taTextColor.getColor(0, Color.WHITE) + taTextColor.recycle() var fragmentTag = LIST_NODES_FRAGMENT_TAG if (currentGroupIsASearch) @@ -209,7 +216,9 @@ class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListen } // Search suggestion - searchSuggestionAdapter = SearchEntryCursorAdapter(this, mDatabase) + mDatabase?.let { database -> + searchSuggestionAdapter = SearchEntryCursorAdapter(this, database) + } Log.i(TAG, "Finished creating tree") } @@ -631,7 +640,9 @@ class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListen setOnSuggestionListener(object : SearchView.OnSuggestionListener { override fun onSuggestionClick(position: Int): Boolean { searchSuggestionAdapter?.let { searchAdapter -> - onNodeClick(searchAdapter.getEntryFromPosition(position)) + searchAdapter.getEntryFromPosition(position)?.let { entry -> + onNodeClick(entry) + } } return true } 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 465c11c04..23bd0b8a0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt @@ -98,10 +98,8 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis setReadOnly(readOnly) setIsASearchResult(isASearchResult) setOnNodeClickListener(nodeClickCallback) - nodeMenuListener?.let { menuListener -> - setActivateContextMenu(true) - setNodeMenuListener(menuListener) - } + setActivateContextMenu(true) + setNodeMenuListener(nodeMenuListener) } prefs = PreferenceManager.getDefaultSharedPreferences(context) } @@ -150,7 +148,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis fun rebuildList() { // Add elements to the list - mainGroup?.let { + mainGroup?.let { mainGroup -> mAdapter?.rebuildList(mainGroup) } listView?.apply { @@ -172,7 +170,9 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis // Tell the adapter to refresh it's list mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore) - mAdapter?.rebuildList(mainGroup) + mainGroup?.let { mainGroup -> + mAdapter?.rebuildList(mainGroup) + } } override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { @@ -224,7 +224,9 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis mAdapter?.addNode(newNode) if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) { //mAdapter.updateLastNodeRegister(newNode); - mAdapter?.rebuildList(mainGroup) + mainGroup?.let { mainGroup -> + mAdapter?.rebuildList(mainGroup) + } } } ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result") } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/BasicViewHolder.java b/app/src/main/java/com/kunzisoft/keepass/adapters/BasicViewHolder.kt similarity index 61% rename from app/src/main/java/com/kunzisoft/keepass/adapters/BasicViewHolder.java rename to app/src/main/java/com/kunzisoft/keepass/adapters/BasicViewHolder.kt index d84d1e0aa..4d5ab9169 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/BasicViewHolder.java +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/BasicViewHolder.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 Jeremy Jamet / Kunzisoft. + * Copyright 2019 Jeremy Jamet / Kunzisoft. * * This file is part of KeePass DX. * @@ -17,21 +17,17 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.adapters; +package com.kunzisoft.keepass.adapters -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; +import android.support.v7.widget.RecyclerView +import android.view.View +import android.widget.ImageView +import android.widget.TextView -abstract class BasicViewHolder extends RecyclerView.ViewHolder { +abstract class BasicViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - View container; - ImageView icon; - TextView text; - TextView subText; - - BasicViewHolder(View itemView) { - super(itemView); - } + var container: View? = null + var icon: ImageView? = null + var text: TextView? = null + var subText: TextView? = null } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryViewHolder.java b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryViewHolder.kt similarity index 59% rename from app/src/main/java/com/kunzisoft/keepass/adapters/EntryViewHolder.java rename to app/src/main/java/com/kunzisoft/keepass/adapters/EntryViewHolder.kt index e24f10497..7b1ffecc1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryViewHolder.java +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryViewHolder.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 Jeremy Jamet / Kunzisoft. + * Copyright 2019 Jeremy Jamet / Kunzisoft. * * This file is part of KeePass DX. * @@ -17,19 +17,18 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.adapters; +package com.kunzisoft.keepass.adapters -import android.view.View; +import android.view.View -import com.kunzisoft.keepass.R; +import com.kunzisoft.keepass.R -class EntryViewHolder extends BasicViewHolder { +internal class EntryViewHolder(itemView: View) : BasicViewHolder(itemView) { - EntryViewHolder(View itemView) { - super(itemView); - container = itemView.findViewById(R.id.entry_container); - icon = itemView.findViewById(R.id.entry_icon); - text = itemView.findViewById(R.id.entry_text); - subText = itemView.findViewById(R.id.entry_subtext); + init { + container = itemView.findViewById(R.id.entry_container) + icon = itemView.findViewById(R.id.entry_icon) + text = itemView.findViewById(R.id.entry_text) + subText = itemView.findViewById(R.id.entry_subtext) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/GroupViewHolder.java b/app/src/main/java/com/kunzisoft/keepass/adapters/GroupViewHolder.kt similarity index 59% rename from app/src/main/java/com/kunzisoft/keepass/adapters/GroupViewHolder.java rename to app/src/main/java/com/kunzisoft/keepass/adapters/GroupViewHolder.kt index 4953188bf..b73ca7d91 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/GroupViewHolder.java +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/GroupViewHolder.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 Jeremy Jamet / Kunzisoft. + * Copyright 2019 Jeremy Jamet / Kunzisoft. * * This file is part of KeePass DX. * @@ -17,19 +17,18 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.adapters; +package com.kunzisoft.keepass.adapters -import android.view.View; +import android.view.View -import com.kunzisoft.keepass.R; +import com.kunzisoft.keepass.R -class GroupViewHolder extends BasicViewHolder { +internal class GroupViewHolder(itemView: View) : BasicViewHolder(itemView) { - GroupViewHolder(View itemView) { - super(itemView); - container = itemView.findViewById(R.id.group_container); - icon = itemView.findViewById(R.id.group_icon); - text = itemView.findViewById(R.id.group_text); - subText = itemView.findViewById(R.id.group_subtext); + init { + container = itemView.findViewById(R.id.group_container) + icon = itemView.findViewById(R.id.group_icon) + text = itemView.findViewById(R.id.group_text) + subText = itemView.findViewById(R.id.group_subtext) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.java b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.java deleted file mode 100644 index aeb4ee340..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright 2018 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.adapters; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.support.annotation.NonNull; -import android.support.v7.util.SortedList; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.util.SortedListAdapterCallback; -import android.util.Log; -import android.view.*; -import android.widget.Toast; -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.app.App; -import com.kunzisoft.keepass.database.SortNodeEnum; -import com.kunzisoft.keepass.database.element.*; -import com.kunzisoft.keepass.settings.PreferencesUtil; -import com.kunzisoft.keepass.utils.Util; - -public class NodeAdapter extends RecyclerView.Adapter { - private static final String TAG = NodeAdapter.class.getName(); - - private SortedList nodeSortedList; - - private Context context; - private LayoutInflater inflater; - private MenuInflater menuInflater; - private float textSize; - private float subtextSize; - private float iconSize; - private SortNodeEnum listSort; - private boolean groupsBeforeSort; - private boolean ascendingSort; - private boolean showUsernames; - - private NodeClickCallback nodeClickCallback; - private NodeMenuListener nodeMenuListener; - private boolean activateContextMenu; - private boolean readOnly; - private boolean isASearchResult; - - private Database database; - - private int iconGroupColor; - private int iconEntryColor; - - /** - * Create node list adapter with contextMenu or not - * @param context Context to use - */ - public NodeAdapter(final Context context, MenuInflater menuInflater) { - this.inflater = LayoutInflater.from(context); - this.menuInflater = menuInflater; - this.context = context; - assignPreferences(); - this.activateContextMenu = false; - this.readOnly = false; - this.isASearchResult = false; - - this.nodeSortedList = new SortedList<>(NodeVersioned.class, new SortedListAdapterCallback(this) { - @Override public int compare(NodeVersioned item1, NodeVersioned item2) { - return listSort.getNodeComparator(ascendingSort, groupsBeforeSort).compare(item1, item2); - } - - @Override public boolean areContentsTheSame(NodeVersioned oldItem, NodeVersioned newItem) { - return oldItem.getTitle().equals(newItem.getTitle()) - && oldItem.getIcon().equals(newItem.getIcon()); - } - - @Override public boolean areItemsTheSame(NodeVersioned item1, NodeVersioned item2) { - return item1.equals(item2); - } - }); - - // Database - this.database = App.Companion.getCurrentDatabase(); - - // Retrieve the color to tint the icon - int[] attrTextColorPrimary = {android.R.attr.textColorPrimary}; - TypedArray taTextColorPrimary = context.getTheme().obtainStyledAttributes(attrTextColorPrimary); - this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK); - taTextColorPrimary.recycle(); - int[] attrTextColor = {android.R.attr.textColor}; // In two times to fix bug compilation - TypedArray taTextColor = context.getTheme().obtainStyledAttributes(attrTextColor); - this.iconEntryColor = taTextColor.getColor(0, Color.BLACK); - taTextColor.recycle(); - } - - public void setReadOnly(boolean readOnly) { - this.readOnly = readOnly; - } - - public void setIsASearchResult(boolean isASearchResult) { - this.isASearchResult = isASearchResult; - } - - public void setActivateContextMenu(boolean activate) { - this.activateContextMenu = activate; - } - - private void assignPreferences() { - float textSizeDefault = Util.getListTextDefaultSize(context); - this.textSize = PreferencesUtil.getListTextSize(context); - this.subtextSize = context.getResources().getInteger(R.integer.list_small_size_default) - * textSize / textSizeDefault; - // Retrieve the icon size - float iconDefaultSize = context.getResources().getDimension(R.dimen.list_icon_size_default); - this.iconSize = iconDefaultSize * textSize / textSizeDefault; - this.listSort = PreferencesUtil.getListSort(context); - this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context); - this.ascendingSort = PreferencesUtil.getAscendingSort(context); - this.showUsernames = PreferencesUtil.showUsernamesListEntries(context); - } - - /** - * Rebuild the list by clear and build children from the group - */ - public void rebuildList(GroupVersioned group) { - this.nodeSortedList.clear(); - assignPreferences(); - // TODO verify sort - try { - this.nodeSortedList.addAll(group.getChildrenWithoutMetaStream()); - } catch (Exception e) { - Log.e(TAG, "Can't add node elements to the list", e); - Toast.makeText(context, "Can't add node elements to the list : " + e.getMessage(), Toast.LENGTH_LONG).show(); - } - } - - /** - * Determine if the adapter contains or not any element - * @return true if the list is empty - */ - public boolean isEmpty() { - return nodeSortedList.size() <= 0; - } - - /** - * Add a node to the list - * @param node Node to add - */ - public void addNode(NodeVersioned node) { - nodeSortedList.add(node); - } - - /** - * Remove a node in the list - * @param node Node to delete - */ - public void removeNode(NodeVersioned node) { - nodeSortedList.remove(node); - } - - /** - * Update a node in the list - * @param oldNode Node before the update - * @param newNode Node after the update - */ - public void updateNode(NodeVersioned oldNode, NodeVersioned newNode) { - nodeSortedList.beginBatchedUpdates(); - nodeSortedList.remove(oldNode); - nodeSortedList.add(newNode); - nodeSortedList.endBatchedUpdates(); - } - - /** - * Notify a change sort of the list - */ - public void notifyChangeSort(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore) { - this.listSort = sortNodeEnum; - this.ascendingSort = ascending; - this.groupsBeforeSort = groupsBefore; - } - - @Override - public int getItemViewType(int position) { - return nodeSortedList.get(position).getType().ordinal(); - } - - @NonNull - @Override - public BasicViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - BasicViewHolder basicViewHolder; - View view; - if (viewType == Type.GROUP.ordinal()) { - view = inflater.inflate(R.layout.list_nodes_group, parent, false); - basicViewHolder = new GroupViewHolder(view); - } else { - view = inflater.inflate(R.layout.list_nodes_entry, parent, false); - basicViewHolder = new EntryViewHolder(view); - } - return basicViewHolder; - } - - @Override - public void onBindViewHolder(@NonNull BasicViewHolder holder, int position) { - NodeVersioned subNode = nodeSortedList.get(position); - // Assign image - int iconColor = Color.BLACK; - switch (subNode.getType()) { - case GROUP: - iconColor = iconGroupColor; - break; - case ENTRY: - iconColor = iconEntryColor; - break; - } - database.getDrawFactory().assignDatabaseIconTo(context, holder.icon, subNode.getIcon(), iconColor); - // Assign text - holder.text.setText(subNode.getTitle()); - // Assign click - holder.container.setOnClickListener( - new OnNodeClickListener(subNode)); - // Context menu - if (activateContextMenu) { - holder.container.setOnCreateContextMenuListener( - new ContextMenuBuilder(subNode, nodeMenuListener, readOnly)); - } - - // Add username - holder.subText.setText(""); - holder.subText.setVisibility(View.GONE); - if (subNode.getType().equals(Type.ENTRY)) { - EntryVersioned entry = (EntryVersioned) subNode; - - database.startManageEntry(entry); - - holder.text.setText(entry.getVisualTitle()); - - String username = entry.getUsername(); - if (showUsernames && !username.isEmpty()) { - holder.subText.setVisibility(View.VISIBLE); - holder.subText.setText(username); - } - - database.stopManageEntry(entry); - } - - // Assign image and text size - // Relative size of the icon - holder.icon.getLayoutParams().height = ((int) iconSize); - holder.icon.getLayoutParams().width = ((int) iconSize); - holder.text.setTextSize(textSize); - holder.subText.setTextSize(subtextSize); - } - - @Override - public int getItemCount() { - return nodeSortedList.size(); - } - - /** - * Assign a listener when a node is clicked - */ - public void setOnNodeClickListener(NodeClickCallback nodeClickCallback) { - this.nodeClickCallback = nodeClickCallback; - } - - /** - * Assign a listener when an element of menu is clicked - */ - public void setNodeMenuListener(NodeMenuListener nodeMenuListener) { - this.nodeMenuListener = nodeMenuListener; - } - - /** - * Callback listener to redefine to do an action when a node is click - */ - public interface NodeClickCallback { - void onNodeClick(NodeVersioned node); - } - - /** - * Menu listener to redefine to do an action in menu - */ - public interface NodeMenuListener { - boolean onOpenMenuClick(NodeVersioned node); - boolean onEditMenuClick(NodeVersioned node); - boolean onCopyMenuClick(NodeVersioned node); - boolean onMoveMenuClick(NodeVersioned node); - boolean onDeleteMenuClick(NodeVersioned node); - } - - /** - * Utility class for node listener - */ - private class OnNodeClickListener implements View.OnClickListener { - private NodeVersioned node; - - OnNodeClickListener(NodeVersioned node) { - this.node = node; - } - - @Override - public void onClick(View v) { - if (nodeClickCallback != null) - nodeClickCallback.onNodeClick(node); - } - } - - /** - * Utility class for menu listener - */ - private class ContextMenuBuilder implements View.OnCreateContextMenuListener { - - private NodeVersioned node; - private NodeMenuListener menuListener; - private boolean readOnly; - - ContextMenuBuilder(NodeVersioned node, NodeMenuListener menuListener, boolean readOnly) { - this.menuListener = menuListener; - this.node = node; - this.readOnly = readOnly; - } - - @Override - public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) { - menuInflater.inflate(R.menu.node_menu, contextMenu); - - // Opening - MenuItem menuItem = contextMenu.findItem(R.id.menu_open); - menuItem.setOnMenuItemClickListener(mOnMyActionClickListener); - - Database database = App.Companion.getCurrentDatabase(); - - // Edition - if (readOnly || node.equals(database.getRecycleBin())) { - contextMenu.removeItem(R.id.menu_edit); - } else { - menuItem = contextMenu.findItem(R.id.menu_edit); - menuItem.setOnMenuItemClickListener(mOnMyActionClickListener); - } - - // Copy (not for group) - if (readOnly - || isASearchResult - || node.equals(database.getRecycleBin()) - || node.getType().equals(Type.GROUP)) { - // TODO COPY For Group - contextMenu.removeItem(R.id.menu_copy); - } else { - menuItem = contextMenu.findItem(R.id.menu_copy); - menuItem.setOnMenuItemClickListener(mOnMyActionClickListener); - } - - // Move - if (readOnly - || isASearchResult - || node.equals(database.getRecycleBin())) { - contextMenu.removeItem(R.id.menu_move); - } else { - menuItem = contextMenu.findItem(R.id.menu_move); - menuItem.setOnMenuItemClickListener(mOnMyActionClickListener); - } - - // Deletion - if (readOnly || node.equals(database.getRecycleBin())) { - contextMenu.removeItem(R.id.menu_delete); - } else { - menuItem = contextMenu.findItem(R.id.menu_delete); - menuItem.setOnMenuItemClickListener(mOnMyActionClickListener); - } - } - - private MenuItem.OnMenuItemClickListener mOnMyActionClickListener = new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - if (menuListener == null) - return false; - switch ( item.getItemId() ) { - case R.id.menu_open: - return menuListener.onOpenMenuClick(node); - case R.id.menu_edit: - return menuListener.onEditMenuClick(node); - case R.id.menu_copy: - return menuListener.onCopyMenuClick(node); - case R.id.menu_move: - return menuListener.onMoveMenuClick(node); - case R.id.menu_delete: - return menuListener.onDeleteMenuClick(node); - default: - return false; - } - } - }; - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt new file mode 100644 index 000000000..555504223 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt @@ -0,0 +1,362 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.adapters + +import android.content.Context +import android.graphics.Color +import android.support.v7.util.SortedList +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.util.SortedListAdapterCallback +import android.util.Log +import android.view.* +import android.widget.Toast +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.app.App +import com.kunzisoft.keepass.database.SortNodeEnum +import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.utils.Util + +class NodeAdapter +/** + * Create node list adapter with contextMenu or not + * @param context Context to use + */ +(private val context: Context, private val menuInflater: MenuInflater) : RecyclerView.Adapter() { + + private val nodeSortedList: SortedList + private val inflater: LayoutInflater = LayoutInflater.from(context) + private var textSize: Float = 0.toFloat() + private var subtextSize: Float = 0.toFloat() + private var iconSize: Float = 0.toFloat() + private var listSort: SortNodeEnum? = null + private var groupsBeforeSort: Boolean = false + private var ascendingSort: Boolean = false + private var showUserNames: Boolean = false + + private var nodeClickCallback: NodeClickCallback? = null + private var nodeMenuListener: NodeMenuListener? = null + private var activateContextMenu: Boolean = false + private var readOnly: Boolean = false + private var isASearchResult: Boolean = false + + private val mDatabase: Database + + private val iconGroupColor: Int + private val iconEntryColor: Int + + /** + * Determine if the adapter contains or not any element + * @return true if the list is empty + */ + val isEmpty: Boolean + get() = nodeSortedList.size() <= 0 + + init { + assignPreferences() + this.activateContextMenu = false + this.readOnly = false + this.isASearchResult = false + + this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback(this) { + override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int { + return listSort?.getNodeComparator(ascendingSort, groupsBeforeSort)?.compare(item1, item2) ?: 0 + } + + override fun areContentsTheSame(oldItem: NodeVersioned, newItem: NodeVersioned): Boolean { + return oldItem.title == newItem.title && oldItem.icon == newItem.icon + } + + override fun areItemsTheSame(item1: NodeVersioned, item2: NodeVersioned): Boolean { + return item1 == item2 + } + }) + + // Database + this.mDatabase = App.currentDatabase + + // Retrieve the color to tint the icon + val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) + this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK) + taTextColorPrimary.recycle() + // In two times to fix bug compilation + val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) + this.iconEntryColor = taTextColor.getColor(0, Color.BLACK) + taTextColor.recycle() + } + + fun setReadOnly(readOnly: Boolean) { + this.readOnly = readOnly + } + + fun setIsASearchResult(isASearchResult: Boolean) { + this.isASearchResult = isASearchResult + } + + fun setActivateContextMenu(activate: Boolean) { + this.activateContextMenu = activate + } + + private fun assignPreferences() { + val textSizeDefault = Util.getListTextDefaultSize(context) + this.textSize = PreferencesUtil.getListTextSize(context) + this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault + // Retrieve the icon size + val iconDefaultSize = context.resources.getDimension(R.dimen.list_icon_size_default) + this.iconSize = iconDefaultSize * textSize / textSizeDefault + this.listSort = PreferencesUtil.getListSort(context) + this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context) + this.ascendingSort = PreferencesUtil.getAscendingSort(context) + this.showUserNames = PreferencesUtil.showUsernamesListEntries(context) + } + + /** + * Rebuild the list by clear and build children from the group + */ + fun rebuildList(group: GroupVersioned) { + this.nodeSortedList.clear() + assignPreferences() + // TODO verify sort + try { + this.nodeSortedList.addAll(group.getChildrenWithoutMetaStream()) + } catch (e: Exception) { + Log.e(TAG, "Can't add node elements to the list", e) + Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show() + } + + } + + /** + * Add a node to the list + * @param node Node to add + */ + fun addNode(node: NodeVersioned) { + nodeSortedList.add(node) + } + + /** + * Remove a node in the list + * @param node Node to delete + */ + fun removeNode(node: NodeVersioned) { + nodeSortedList.remove(node) + } + + /** + * Update a node in the list + * @param oldNode Node before the update + * @param newNode Node after the update + */ + fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) { + nodeSortedList.beginBatchedUpdates() + nodeSortedList.remove(oldNode) + nodeSortedList.add(newNode) + nodeSortedList.endBatchedUpdates() + } + + /** + * Notify a change sort of the list + */ + fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) { + this.listSort = sortNodeEnum + this.ascendingSort = ascending + this.groupsBeforeSort = groupsBefore + } + + override fun getItemViewType(position: Int): Int { + return nodeSortedList.get(position).type.ordinal + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicViewHolder { + val basicViewHolder: BasicViewHolder + val view: View + if (viewType == Type.GROUP.ordinal) { + view = inflater.inflate(R.layout.list_nodes_group, parent, false) + basicViewHolder = GroupViewHolder(view) + } else { + view = inflater.inflate(R.layout.list_nodes_entry, parent, false) + basicViewHolder = EntryViewHolder(view) + } + return basicViewHolder + } + + override fun onBindViewHolder(holder: BasicViewHolder, position: Int) { + val subNode = nodeSortedList.get(position) + // Assign image + val iconColor = when (subNode.type) { + Type.GROUP -> iconGroupColor + Type.ENTRY -> iconEntryColor + } + mDatabase.drawFactory.assignDatabaseIconTo(context, holder.icon, subNode.icon, iconColor) + // Assign text + holder.text?.text = subNode.title + // Assign click + holder.container?.setOnClickListener( + OnNodeClickListener(subNode)) + // Context menu + if (activateContextMenu) { + holder.container?.setOnCreateContextMenuListener( + ContextMenuBuilder(subNode, nodeMenuListener, readOnly)) + } + + // Add username + holder.subText?.text = "" + holder.subText?.visibility = View.GONE + if (subNode.type == Type.ENTRY) { + val entry = subNode as EntryVersioned + + mDatabase.startManageEntry(entry) + + holder.text?.text = entry.getVisualTitle() + + val username = entry.username + if (showUserNames && username.isNotEmpty()) { + holder.subText?.visibility = View.VISIBLE + holder.subText?.text = username + } + + mDatabase.stopManageEntry(entry) + } + + // Assign image and text size + // Relative size of the icon + holder.icon?.layoutParams?.height = iconSize.toInt() + holder.icon?.layoutParams?.width = iconSize.toInt() + holder.text?.textSize = textSize + holder.subText?.textSize = subtextSize + } + + override fun getItemCount(): Int { + return nodeSortedList.size() + } + + /** + * Assign a listener when a node is clicked + */ + fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) { + this.nodeClickCallback = nodeClickCallback + } + + /** + * Assign a listener when an element of menu is clicked + */ + fun setNodeMenuListener(nodeMenuListener: NodeMenuListener?) { + this.nodeMenuListener = nodeMenuListener + } + + /** + * Callback listener to redefine to do an action when a node is click + */ + interface NodeClickCallback { + fun onNodeClick(node: NodeVersioned) + } + + /** + * Menu listener to redefine to do an action in menu + */ + interface NodeMenuListener { + fun onOpenMenuClick(node: NodeVersioned): Boolean + fun onEditMenuClick(node: NodeVersioned): Boolean + fun onCopyMenuClick(node: NodeVersioned): Boolean + fun onMoveMenuClick(node: NodeVersioned): Boolean + fun onDeleteMenuClick(node: NodeVersioned): Boolean + } + + /** + * Utility class for node listener + */ + private inner class OnNodeClickListener internal constructor(private val node: NodeVersioned) : View.OnClickListener { + + override fun onClick(v: View) { + nodeClickCallback?.onNodeClick(node) + } + } + + /** + * Utility class for menu listener + */ + private inner class ContextMenuBuilder internal constructor(private val node: NodeVersioned, private val menuListener: NodeMenuListener?, private val readOnly: Boolean) : View.OnCreateContextMenuListener { + + private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item -> + if (menuListener == null) + return@OnMenuItemClickListener false + when (item.itemId) { + R.id.menu_open -> menuListener.onOpenMenuClick(node) + R.id.menu_edit -> menuListener.onEditMenuClick(node) + R.id.menu_copy -> menuListener.onCopyMenuClick(node) + R.id.menu_move -> menuListener.onMoveMenuClick(node) + R.id.menu_delete -> menuListener.onDeleteMenuClick(node) + else -> false + } + } + + override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo) { + menuInflater.inflate(R.menu.node_menu, contextMenu) + + // Opening + var menuItem = contextMenu.findItem(R.id.menu_open) + menuItem.setOnMenuItemClickListener(mOnMyActionClickListener) + + val database = App.currentDatabase + + // Edition + if (readOnly || node == database.recycleBin) { + contextMenu.removeItem(R.id.menu_edit) + } else { + menuItem = contextMenu.findItem(R.id.menu_edit) + menuItem.setOnMenuItemClickListener(mOnMyActionClickListener) + } + + // Copy (not for group) + if (readOnly + || isASearchResult + || node == database.recycleBin + || node.type == Type.GROUP) { + // TODO COPY For Group + contextMenu.removeItem(R.id.menu_copy) + } else { + menuItem = contextMenu.findItem(R.id.menu_copy) + menuItem.setOnMenuItemClickListener(mOnMyActionClickListener) + } + + // Move + if (readOnly + || isASearchResult + || node == database.recycleBin) { + contextMenu.removeItem(R.id.menu_move) + } else { + menuItem = contextMenu.findItem(R.id.menu_move) + menuItem.setOnMenuItemClickListener(mOnMyActionClickListener) + } + + // Deletion + if (readOnly || node == database.recycleBin) { + contextMenu.removeItem(R.id.menu_delete) + } else { + menuItem = contextMenu.findItem(R.id.menu_delete) + menuItem.setOnMenuItemClickListener(mOnMyActionClickListener) + } + } + } + + companion object { + private val TAG = NodeAdapter::class.java.name + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.java b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.java deleted file mode 100644 index 7e11dd460..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2018 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.adapters; - -import android.content.Context; -import android.content.res.TypedArray; -import android.database.Cursor; -import android.graphics.Color; -import android.support.v4.widget.CursorAdapter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.database.cursor.EntryCursor; -import com.kunzisoft.keepass.database.element.Database; -import com.kunzisoft.keepass.database.element.EntryVersioned; -import com.kunzisoft.keepass.database.element.PwIcon; -import com.kunzisoft.keepass.database.element.PwIconFactory; -import com.kunzisoft.keepass.settings.PreferencesUtil; - -import java.util.UUID; - -public class SearchEntryCursorAdapter extends CursorAdapter { - - private LayoutInflater cursorInflater; - private Database database; - private boolean displayUsername; - private int iconColor; - - public SearchEntryCursorAdapter(Context context, Database database) { - super(context, null, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); - cursorInflater = (LayoutInflater) context.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - this.database = database; - - // Get the icon color - int[] attrTextColor = {R.attr.textColorInverse}; - TypedArray taTextColor = context.getTheme().obtainStyledAttributes(attrTextColor); - this.iconColor = taTextColor.getColor(0, Color.WHITE); - taTextColor.recycle(); - - reInit(context); - } - - public void reInit(Context context) { - this.displayUsername = PreferencesUtil.showUsernamesListEntries(context); - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - - View view = cursorInflater.inflate(R.layout.search_entry, parent ,false); - ViewHolder viewHolder = new ViewHolder(); - viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon); - viewHolder.textViewTitle = view.findViewById(R.id.entry_text); - viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext); - view.setTag(viewHolder); - - return view; - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - - // Retrieve elements from cursor - UUID uuid = new UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)), - cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS))); - PwIconFactory iconFactory = database.getIconFactory(); - PwIcon icon = iconFactory.getIcon( - new UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)), - cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS)))); - if (icon.isUnknown()) { - icon = iconFactory.getIcon(cursor.getInt(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_STANDARD))); - if (icon.isUnknown()) - icon = iconFactory.getKeyIcon(); - } - String title = cursor.getString( cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_TITLE) ); - String username = cursor.getString( cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_USERNAME) ); - String url = cursor.getString( cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_URL) ); - - ViewHolder viewHolder = (ViewHolder) view.getTag(); - - // Assign image - database.getDrawFactory().assignDatabaseIconTo(context, viewHolder.imageViewIcon, icon, iconColor); - - // Assign title - String showTitle = EntryVersioned.CREATOR.getVisualTitle(false, title, username, url, uuid.toString()); - viewHolder.textViewTitle.setText(showTitle); - if (displayUsername && !username.isEmpty()) { - viewHolder.textViewSubTitle.setText(String.format("(%s)", username)); - } else { - viewHolder.textViewSubTitle.setText(""); - } - } - - private static class ViewHolder { - ImageView imageViewIcon; - TextView textViewTitle; - TextView textViewSubTitle; - } - - @Override - public Cursor runQueryOnBackgroundThread(CharSequence constraint) { - return database.searchEntry(constraint.toString()); - } - - public EntryVersioned getEntryFromPosition(int position) { - EntryVersioned pwEntry = null; - - Cursor cursor = this.getCursor(); - if (cursor.moveToFirst() - && cursor.move(position)) { - pwEntry = database.getEntryFrom(cursor); - } - return pwEntry; - } - -} diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt new file mode 100644 index 000000000..a9b7f58c7 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.adapters + +import android.content.Context +import android.database.Cursor +import android.graphics.Color +import android.support.v4.widget.CursorAdapter +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.cursor.EntryCursor +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.EntryVersioned +import com.kunzisoft.keepass.database.element.PwIcon +import com.kunzisoft.keepass.settings.PreferencesUtil +import java.util.* + +class SearchEntryCursorAdapter(context: Context, private val database: Database) : CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) { + + private val cursorInflater: LayoutInflater = context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + private var displayUsername: Boolean = false + private val iconColor: Int + + init { + // Get the icon color + val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) + this.iconColor = taTextColor.getColor(0, Color.WHITE) + taTextColor.recycle() + + reInit(context) + } + + fun reInit(context: Context) { + this.displayUsername = PreferencesUtil.showUsernamesListEntries(context) + } + + override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View { + + val view = cursorInflater.inflate(R.layout.search_entry, parent, false) + val viewHolder = ViewHolder() + viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon) + viewHolder.textViewTitle = view.findViewById(R.id.entry_text) + viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext) + view.tag = viewHolder + + return view + } + + override fun bindView(view: View, context: Context, cursor: Cursor) { + + // Retrieve elements from cursor + val uuid = UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)), + cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS))) + val iconFactory = database.iconFactory + var icon: PwIcon = iconFactory.getIcon( + UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)), + cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS)))) + if (icon.isUnknown) { + icon = iconFactory.getIcon(cursor.getInt(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_STANDARD))) + if (icon.isUnknown) + icon = iconFactory.keyIcon + } + val title = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_TITLE)) + val username = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_USERNAME)) + val url = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_URL)) + + val viewHolder = view.tag as ViewHolder + + // Assign image + database.drawFactory.assignDatabaseIconTo(context, viewHolder.imageViewIcon, icon, iconColor) + + // Assign title + val showTitle = EntryVersioned.getVisualTitle(false, title, username, url, uuid.toString()) + viewHolder.textViewTitle?.text = showTitle + if (displayUsername && username.isNotEmpty()) { + viewHolder.textViewSubTitle?.text = String.format("(%s)", username) + } else { + viewHolder.textViewSubTitle?.text = "" + } + } + + private class ViewHolder { + internal var imageViewIcon: ImageView? = null + internal var textViewTitle: TextView? = null + internal var textViewSubTitle: TextView? = null + } + + override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? { + return database.searchEntry(constraint.toString()) + } + + fun getEntryFromPosition(position: Int): EntryVersioned? { + var pwEntry: EntryVersioned? = null + + val cursor = this.cursor + if (cursor.moveToFirst() && cursor.move(position)) { + pwEntry = database.getEntryFrom(cursor) + } + return pwEntry + } + +} diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/GroupEditDialogFragment.java b/app/src/main/java/com/kunzisoft/keepass/dialogs/GroupEditDialogFragment.java index fa576a400..04c393b58 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/GroupEditDialogFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/dialogs/GroupEditDialogFragment.java @@ -116,6 +116,7 @@ public class GroupEditDialogFragment extends DialogFragment int[] attrs = {android.R.attr.textColorPrimary}; TypedArray ta = getActivity().getTheme().obtainStyledAttributes(attrs); iconColor = ta.getColor(0, Color.WHITE); + ta.recycle(); // Init elements database = App.Companion.getCurrentDatabase(); diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java b/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java index 3f819093c..b7a753ca0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java @@ -139,6 +139,7 @@ public class IconPickerDialogFragment extends DialogFragment { TypedArray ta = getContext().getTheme().obtainStyledAttributes(attrs); int iconColor = ta.getColor(0, Color.BLACK); ImageViewCompat.setImageTintList(iv, ColorStateList.valueOf(iconColor)); + ta.recycle(); } return currView; diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt index cc38c8bcf..0be3664f4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt @@ -81,7 +81,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context, attrs: Attri val attrColorAccent = intArrayOf(R.attr.colorAccentCompat) val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent) - this.colorAccent = taColorAccent.getColor(0, Color.BLACK) + colorAccent = taColorAccent.getColor(0, Color.BLACK) taColorAccent.recycle() } From f36849e815748db705d518aaaaf8f130e35a6351 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 7 Jul 2019 21:39:27 +0200 Subject: [PATCH 13/24] Kotlinized Autofill and BackupAgent --- .../keepass/autofill/AutofillHelper.kt | 45 +++---- .../keepass/autofill/KeeAutofillService.java | 87 ------------- .../keepass/autofill/KeeAutofillService.kt | 71 +++++++++++ .../keepass/autofill/StructureParser.java | 115 ------------------ .../keepass/autofill/StructureParser.kt | 113 +++++++++++++++++ ...ackupAgent.java => SettingsBackupAgent.kt} | 27 ++-- 6 files changed, 221 insertions(+), 237 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt delete mode 100644 app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt rename app/src/main/java/com/kunzisoft/keepass/backup/{SettingsBackupAgent.java => SettingsBackupAgent.kt} (53%) diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt index 7eaf623d4..97ecaca8c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt @@ -52,11 +52,11 @@ object AutofillHelper { } private fun makeEntryTitle(entry: EntryVersioned): String { - if (!entry.title.isEmpty() && !entry.username.isEmpty()) + if (entry.title.isNotEmpty() && entry.username.isNotEmpty()) return String.format("%s (%s)", entry.title, entry.username) - if (!entry.title.isEmpty()) + if (entry.title.isNotEmpty()) return entry.title - if (!entry.username.isEmpty()) + if (entry.username.isNotEmpty()) return entry.username return if (!entry.notes.isEmpty()) entry.notes.trim { it <= ' ' } else "" // TODO No title @@ -89,23 +89,26 @@ object AutofillHelper { * Method to hit when right key is selected */ fun buildResponseWhenEntrySelected(activity: Activity, entry: EntryVersioned) { - val mReplyIntent: Intent - activity.intent?.let { intent -> - if (intent.extras.containsKey(ASSIST_STRUCTURE)) { - val structure = intent.getParcelableExtra(ASSIST_STRUCTURE) - val result = StructureParser(structure).parse() - - // New Response - val responseBuilder = FillResponse.Builder() - val dataset = buildDataset(activity, entry, result) - responseBuilder.addDataset(dataset) - mReplyIntent = Intent() - Log.d(activity.javaClass.name, "Successed Autofill auth.") - mReplyIntent.putExtra( - AutofillManager.EXTRA_AUTHENTICATION_RESULT, - responseBuilder.build()) - activity.setResult(Activity.RESULT_OK, mReplyIntent) - } else { + var setResultOk = false + activity.intent?.extras?.let { extras -> + if (extras.containsKey(ASSIST_STRUCTURE)) { + activity.intent?.getParcelableExtra(ASSIST_STRUCTURE)?.let { structure -> + StructureParser(structure).parse()?.let { result -> + // New Response + val responseBuilder = FillResponse.Builder() + val dataset = buildDataset(activity, entry, result) + responseBuilder.addDataset(dataset) + val mReplyIntent = Intent() + Log.d(activity.javaClass.name, "Successed Autofill auth.") + mReplyIntent.putExtra( + AutofillManager.EXTRA_AUTHENTICATION_RESULT, + responseBuilder.build()) + setResultOk = true + activity.setResult(Activity.RESULT_OK, mReplyIntent) + } + } + } + if (!setResultOk) { Log.w(activity.javaClass.name, "Failed Autofill auth.") activity.setResult(Activity.RESULT_CANCELED) } @@ -118,7 +121,7 @@ object AutofillHelper { fun startActivityForAutofillResult(activity: Activity, intent: Intent, assistStructure: AssistStructure) { EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent) intent.putExtra(ASSIST_STRUCTURE, assistStructure) - activity.startActivityForResult(intent, AutofillHelper.AUTOFILL_RESPONSE_REQUEST_CODE) + activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE) } /** diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.java b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.java deleted file mode 100644 index eb62f6a48..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2017 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.autofill; - -import android.app.assist.AssistStructure; -import android.content.IntentSender; -import android.os.Build; -import android.os.CancellationSignal; -import android.service.autofill.AutofillService; -import android.service.autofill.FillCallback; -import android.service.autofill.FillContext; -import android.service.autofill.FillRequest; -import android.service.autofill.FillResponse; -import android.service.autofill.SaveCallback; -import android.service.autofill.SaveRequest; -import android.support.annotation.NonNull; -import android.support.annotation.RequiresApi; -import android.util.Log; -import android.view.autofill.AutofillId; -import android.widget.RemoteViews; - -import com.kunzisoft.keepass.R; - -import java.util.Arrays; -import java.util.List; - -@RequiresApi(api = Build.VERSION_CODES.O) -public class KeeAutofillService extends AutofillService { - private static final String TAG = "KeeAutofillService"; - - @Override - public void onFillRequest(@NonNull FillRequest request, @NonNull CancellationSignal cancellationSignal, - @NonNull FillCallback callback) { - List fillContexts = request.getFillContexts(); - AssistStructure latestStructure = fillContexts.get(fillContexts.size() - 1).getStructure(); - - cancellationSignal.setOnCancelListener(() -> - Log.e(TAG, "Cancel autofill not implemented in this sample.") - ); - - FillResponse.Builder responseBuilder = new FillResponse.Builder(); - // Check user's settings for authenticating Responses and Datasets. - StructureParser.Result parseResult = new StructureParser(latestStructure).parse(); - AutofillId[] autofillIds = parseResult.allAutofillIds(); - if (!Arrays.asList(autofillIds).isEmpty()) { - // If the entire Autofill Response is authenticated, AuthActivity is used - // to generate Response. - IntentSender sender = AutoFillLauncherActivity.Companion.getAuthIntentSenderForResponse(this); - RemoteViews presentation = new RemoteViews(getPackageName(), R.layout.autofill_service_unlock); - responseBuilder.setAuthentication(autofillIds, sender, presentation); - callback.onSuccess(responseBuilder.build()); - } - } - - @Override - public void onSaveRequest(@NonNull SaveRequest request, @NonNull SaveCallback callback) { - // TODO Save autofill - //callback.onFailure(getString(R.string.autofill_not_support_save)); - } - - @Override - public void onConnected() { - Log.d(TAG, "onConnected"); - } - - @Override - public void onDisconnected() { - Log.d(TAG, "onDisconnected"); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt new file mode 100644 index 000000000..baa50588c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.autofill + +import android.os.Build +import android.os.CancellationSignal +import android.service.autofill.* +import android.support.annotation.RequiresApi +import android.util.Log +import android.widget.RemoteViews +import com.kunzisoft.keepass.R + +@RequiresApi(api = Build.VERSION_CODES.O) +class KeeAutofillService : AutofillService() { + + override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, + callback: FillCallback) { + val fillContexts = request.fillContexts + val latestStructure = fillContexts[fillContexts.size - 1].structure + + cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") } + + val responseBuilder = FillResponse.Builder() + // Check user's settings for authenticating Responses and Datasets. + val parseResult = StructureParser(latestStructure).parse() + parseResult?.allAutofillIds()?.let { autofillIds -> + if (listOf(*autofillIds).isNotEmpty()) { + // If the entire Autofill Response is authenticated, AuthActivity is used + // to generate Response. + val sender = AutoFillLauncherActivity.getAuthIntentSenderForResponse(this) + val presentation = RemoteViews(packageName, R.layout.autofill_service_unlock) + responseBuilder.setAuthentication(autofillIds, sender, presentation) + callback.onSuccess(responseBuilder.build()) + } + } + } + + override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { + // TODO Save autofill + //callback.onFailure(getString(R.string.autofill_not_support_save)); + } + + override fun onConnected() { + Log.d(TAG, "onConnected") + } + + override fun onDisconnected() { + Log.d(TAG, "onDisconnected") + } + + companion object { + private val TAG = KeeAutofillService::class.java.name + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.java b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.java deleted file mode 100644 index e3af9fbcc..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2018 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.autofill; - -import android.app.assist.AssistStructure; -import android.os.Build; -import android.support.annotation.RequiresApi; -import android.text.InputType; -import android.util.Log; -import android.view.View; -import android.view.autofill.AutofillId; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - - -/** - * Parse AssistStructure and guess username and password fields. - */ -@RequiresApi(api = Build.VERSION_CODES.O) -class StructureParser { - static private final String TAG = StructureParser.class.getName(); - - final private AssistStructure structure; - private Result result; - private AutofillId usernameCandidate; - - StructureParser(AssistStructure structure) { - this.structure = structure; - } - - Result parse() { - result = new Result(); - usernameCandidate = null; - for (int i=0; i 0) { - if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_USERNAME::equals)) - result.username.add(node.getAutofillId()); - else if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_EMAIL_ADDRESS::equals)) - result.email.add(node.getAutofillId()); - else if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_PASSWORD::equals)) - result.password.add(node.getAutofillId()); - else - Log.d(TAG, "unsupported hints"); - } else if (node.getAutofillType() == View.AUTOFILL_TYPE_TEXT) { - int inputType = node.getInputType(); - if ((inputType & InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) > 0) - result.email.add(node.getAutofillId()); - else if ((inputType & InputType.TYPE_TEXT_VARIATION_PASSWORD) > 0) - result.password.add(node.getAutofillId()); - else if (result.password.isEmpty()) - usernameCandidate = node.getAutofillId(); - } - - for (int i=0; i title; - final List webDomain; - final List username; - final List email; - final List password; - - private Result() { - title = new ArrayList<>(); - webDomain = new ArrayList<>(); - username = new ArrayList<>(); - email = new ArrayList<>(); - password = new ArrayList<>(); - } - - AutofillId[] allAutofillIds() { - ArrayList all = new ArrayList<>(); - all.addAll(username); - all.addAll(email); - all.addAll(password); - return all.toArray(new AutofillId[0]); - } - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt new file mode 100644 index 000000000..7b95b069a --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + */ +package com.kunzisoft.keepass.autofill + +import android.app.assist.AssistStructure +import android.os.Build +import android.support.annotation.RequiresApi +import android.text.InputType +import android.util.Log +import android.view.View +import android.view.autofill.AutofillId +import java.util.* + + +/** + * Parse AssistStructure and guess username and password fields. + */ +@RequiresApi(api = Build.VERSION_CODES.O) +internal class StructureParser(private val structure: AssistStructure) { + private var result: Result? = null + private var usernameCandidate: AutofillId? = null + + fun parse(): Result? { + result = Result() + result?.apply { + usernameCandidate = null + for (i in 0 until structure.windowNodeCount) { + val windowNode = structure.getWindowNodeAt(i) + title.add(windowNode.title) + windowNode.rootViewNode.webDomain?.let { + webDomain.add(it) + } + parseViewNode(windowNode.rootViewNode) + } + // If not explicit username field found, add the field just before password field. + if (username.isEmpty() && email.isEmpty() + && password.isNotEmpty() && usernameCandidate != null) + username.add(usernameCandidate!!) + } + + return result + } + + private fun parseViewNode(node: AssistStructure.ViewNode) { + val hints = node.autofillHints + val autofillId = node.autofillId + if (autofillId != null) { + if (hints != null && hints.isNotEmpty()) { + when { + Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId) + Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId) + Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId) + else -> Log.d(TAG, "unsupported hints") + } + } else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) { + val inputType = node.inputType + when { + inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId) + inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId) + result?.password?.isEmpty() == true -> usernameCandidate = autofillId + } + } + } + + for (i in 0 until node.childCount) + parseViewNode(node.getChildAt(i)) + } + + @RequiresApi(api = Build.VERSION_CODES.O) + internal class Result { + val title: MutableList + val webDomain: MutableList + val username: MutableList + val email: MutableList + val password: MutableList + + init { + title = ArrayList() + webDomain = ArrayList() + username = ArrayList() + email = ArrayList() + password = ArrayList() + } + + fun allAutofillIds(): Array { + val all = ArrayList() + all.addAll(username) + all.addAll(email) + all.addAll(password) + return all.toTypedArray() + } + } + + companion object { + private val TAG = StructureParser::class.java.name + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/backup/SettingsBackupAgent.java b/app/src/main/java/com/kunzisoft/keepass/backup/SettingsBackupAgent.kt similarity index 53% rename from app/src/main/java/com/kunzisoft/keepass/backup/SettingsBackupAgent.java rename to app/src/main/java/com/kunzisoft/keepass/backup/SettingsBackupAgent.kt index a770898f7..5adeadddc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/backup/SettingsBackupAgent.java +++ b/app/src/main/java/com/kunzisoft/keepass/backup/SettingsBackupAgent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. + * Copyright 2019 Jeremy Jamet / Kunzisoft. * * This file is part of KeePass DX. * @@ -17,23 +17,22 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.backup; +package com.kunzisoft.keepass.backup -import android.annotation.SuppressLint; -import android.app.backup.BackupAgentHelper; -import android.app.backup.SharedPreferencesBackupHelper; +import android.annotation.SuppressLint +import android.app.backup.BackupAgentHelper +import android.app.backup.SharedPreferencesBackupHelper @SuppressLint("NewApi") -public class SettingsBackupAgent extends BackupAgentHelper { +class SettingsBackupAgent : BackupAgentHelper() { - private static final String PREFS_BACKUP_KEY = "prefs"; - - @Override - public void onCreate() { - String defaultPrefs = this.getPackageName() + "_preferences"; - - SharedPreferencesBackupHelper prefHelper = new SharedPreferencesBackupHelper(this, defaultPrefs); - addHelper(PREFS_BACKUP_KEY, prefHelper); + override fun onCreate() { + val defaultPrefs = this.packageName + "_preferences" + val prefHelper = SharedPreferencesBackupHelper(this, defaultPrefs) + addHelper(PREFS_BACKUP_KEY, prefHelper) } + companion object { + private const val PREFS_BACKUP_KEY = "prefs" + } } From 63f261f169e0d841ca081d0da65275ec9703726a Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 9 Jul 2019 17:12:00 +0200 Subject: [PATCH 14/24] Fix add new entry --- .../java/com/kunzisoft/keepass/activities/GroupActivity.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 117235011..f985e8d2c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -209,8 +209,8 @@ class GroupActivity : LockingActivity(), .show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP) } - mCurrentGroup?.let { currentGroup -> - addNodeButtonView?.setAddEntryClickListener { + addNodeButtonView?.setAddEntryClickListener { + mCurrentGroup?.let { currentGroup -> EntryEditActivity.launch(this@GroupActivity, currentGroup) } } @@ -971,7 +971,6 @@ class GroupActivity : LockingActivity(), } override fun onBackPressed() { - // Normal way when we are not in root if (mRootGroup != null && mRootGroup != mCurrentGroup) super.onBackPressed() From 7ad43dbed78f799f27271db89560f5cf8ec04352 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 10 Jul 2019 17:47:06 +0200 Subject: [PATCH 15/24] Kotlinized Activities --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 6 +- .../keepass/activities/AboutActivity.kt | 2 +- .../activities/FileDatabaseSelectActivity.kt | 557 ++++++++++ .../keepass/activities/ListNodesFragment.kt | 2 +- .../keepass/activities/PasswordActivity.kt | 895 ++++++++++++++++ .../activities/lock/LockingActivity.kt | 2 +- .../stylish}/FilePickerStylishActivity.java | 3 +- .../{ => activities}/stylish/Stylish.java | 2 +- .../stylish/StylishActivity.java | 2 +- .../stylish/StylishFragment.java | 2 +- .../utilities}/UriIntentInitTask.kt | 6 +- .../utilities/UriIntentInitTaskCallback.kt} | 8 +- .../kunzisoft/keepass/adapters/NodeAdapter.kt | 57 +- .../java/com/kunzisoft/keepass/app/App.kt | 2 +- .../autofill/AutoFillLauncherActivity.kt | 2 +- .../dialogs/CreateFileDialogFragment.java | 2 +- .../dialogs/IconPickerDialogFragment.java | 2 +- .../DeleteFileHistoryAsyncTask.java | 54 - .../fileselect/DeleteFileHistoryAsyncTask.kt | 42 + .../FileDatabaseHistoryAdapter.java | 181 ---- .../fileselect/FileDatabaseHistoryAdapter.kt | 156 +++ .../keepass/fileselect/FileDatabaseModel.java | 2 +- .../FileDatabaseSelectActivity.java | 602 ----------- .../fileselect/OpenFileHistoryAsyncTask.java | 52 - ...wHolder.kt => OpenFileHistoryAsyncTask.kt} | 28 +- .../database/FileDatabaseHistory.kt | 5 +- .../magikeyboard/KeyboardLauncherActivity.kt | 2 +- .../NotificationCopyingService.java | 2 +- .../keepass/password/IntentBuildLauncher.java | 7 - .../keepass/password/PasswordActivity.java | 998 ------------------ .../keepass/settings/MagikIMESettings.java | 2 +- .../settings/NestedSettingsFragment.java | 2 +- .../com/kunzisoft/keepass/utils/MenuUtil.kt | 2 +- 34 files changed, 1730 insertions(+), 1964 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt rename app/src/main/java/com/kunzisoft/keepass/{fileselect => activities/stylish}/FilePickerStylishActivity.java (96%) rename app/src/main/java/com/kunzisoft/keepass/{ => activities}/stylish/Stylish.java (98%) rename app/src/main/java/com/kunzisoft/keepass/{ => activities}/stylish/StylishActivity.java (96%) rename app/src/main/java/com/kunzisoft/keepass/{ => activities}/stylish/StylishFragment.java (98%) rename app/src/main/java/com/kunzisoft/keepass/{password => activities/utilities}/UriIntentInitTask.kt (94%) rename app/src/main/java/com/kunzisoft/keepass/{password/UriIntentInitTaskCallback.java => activities/utilities/UriIntentInitTaskCallback.kt} (79%) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.kt delete mode 100644 app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.kt delete mode 100644 app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.java rename app/src/main/java/com/kunzisoft/keepass/fileselect/{FileDatabaseHistoryViewHolder.kt => OpenFileHistoryAsyncTask.kt} (51%) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/password/IntentBuildLauncher.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java diff --git a/app/build.gradle b/app/build.gradle index 10482c38c..24fb0a3e3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' android { compileSdkVersion 27 @@ -80,7 +81,7 @@ android { def supportVersion = "27.1.1" def spongycastleVersion = "1.58.0.0" -def permissionDispatcherVersion = "3.1.0" +def permissionDispatcherVersion = "3.3.1" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -103,7 +104,7 @@ dependencies { // if you don't use android.app.Fragment you can exclude support for them exclude module: "support-v13" } - annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion" + kapt "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion" // Apache Commons Collections implementation 'commons-collections:commons-collections:3.2.1' implementation 'org.apache.commons:commons-io:1.3.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 399f808f7..3789cf299 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ android:value="" /> @@ -89,7 +89,7 @@ android:resource="@xml/nnf_provider_paths" /> 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 c2018c06c..6593f3719 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt @@ -28,7 +28,7 @@ import android.widget.TextView import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.stylish.StylishActivity +import com.kunzisoft.keepass.activities.stylish.StylishActivity import org.joda.time.DateTime diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt new file mode 100644 index 000000000..100f7ba71 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -0,0 +1,557 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.activities + +import android.Manifest +import android.app.Activity +import android.app.assist.AssistStructure +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.os.Handler +import android.preference.PreferenceManager +import android.support.annotation.RequiresApi +import android.support.v7.app.AlertDialog +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.Toolbar +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.stylish.StylishActivity +import com.kunzisoft.keepass.autofill.AutofillHelper +import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable +import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable +import com.kunzisoft.keepass.database.action.ProgressDialogRunnable +import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment +import com.kunzisoft.keepass.dialogs.CreateFileDialogFragment +import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation +import com.kunzisoft.keepass.fileselect.* +import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory +import com.kunzisoft.keepass.magikeyboard.KeyboardHelper +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.utils.EmptyUtils +import com.kunzisoft.keepass.utils.MenuUtil +import com.kunzisoft.keepass.utils.UriUtil +import net.cachapa.expandablelayout.ExpandableLayout +import permissions.dispatcher.* +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.lang.ref.WeakReference +import java.net.URLDecoder +import java.util.* + +@RuntimePermissions +class FileDatabaseSelectActivity : StylishActivity(), + CreateFileDialogFragment.DefinePathDialogListener, + AssignMasterKeyDialogFragment.AssignPasswordDialogListener, + FileDatabaseHistoryAdapter.FileItemOpenListener, + FileDatabaseHistoryAdapter.FileSelectClearListener, + FileDatabaseHistoryAdapter.FileInformationShowListener { + + // Views + private var fileListContainer: View? = null + private var createButtonView: View? = null + private var browseButtonView: View? = null + private var openButtonView: View? = null + private var fileSelectExpandableButtonView: View? = null + private var fileSelectExpandableLayout: ExpandableLayout? = null + private var openFileNameView: EditText? = null + + // Adapter to manage database history list + private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null + + private var mFileDatabaseHistory: FileDatabaseHistory? = null + + private var mDatabaseFileUri: Uri? = null + + private var mKeyFileHelper: KeyFileHelper? = null + + private var mDefaultPath: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + mFileDatabaseHistory = FileDatabaseHistory.getInstance(WeakReference(applicationContext)) + + setContentView(R.layout.file_selection) + fileListContainer = findViewById(R.id.container_file_list) + + val toolbar = findViewById(R.id.toolbar) + toolbar.title = "" + setSupportActionBar(toolbar) + + openFileNameView = findViewById(R.id.file_filename) + + // Set the initial value of the filename + mDefaultPath = (Environment.getExternalStorageDirectory().absolutePath + + getString(R.string.database_file_path_default) + + getString(R.string.database_file_name_default) + + getString(R.string.database_file_extension_default)) + openFileNameView?.setHint(R.string.open_link_database) + + // Button to expand file selection + fileSelectExpandableButtonView = findViewById(R.id.file_select_expandable_button) + fileSelectExpandableLayout = findViewById(R.id.file_select_expandable) + fileSelectExpandableButtonView?.setOnClickListener { _ -> + if (fileSelectExpandableLayout?.isExpanded == true) + fileSelectExpandableLayout?.collapse() + else + fileSelectExpandableLayout?.expand() + } + + // History list + val databaseFileListView = findViewById(R.id.file_list) + databaseFileListView.layoutManager = LinearLayoutManager(this) + + // Open button + openButtonView = findViewById(R.id.open_database) + openButtonView?.setOnClickListener { _ -> + var fileName = openFileNameView?.text?.toString() ?: "" + mDefaultPath?.let { + if (fileName.isEmpty()) + fileName = it + } + launchPasswordActivityWithPath(fileName) + } + + // Create button + createButtonView = findViewById(R.id.create_database) + createButtonView?.setOnClickListener { openCreateFileDialogFragmentWithPermissionCheck() } + + mKeyFileHelper = KeyFileHelper(this) + browseButtonView = findViewById(R.id.browse_button) + browseButtonView?.setOnClickListener(mKeyFileHelper!!.getOpenFileOnClickViewListener { Uri.parse("file://" + openFileNameView!!.text.toString()) }) + + // Construct adapter with listeners + mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this@FileDatabaseSelectActivity, + mFileDatabaseHistory?.databaseUriList ?: ArrayList()) + mAdapterDatabaseHistory?.setOnItemClickListener(this) + mAdapterDatabaseHistory?.setFileSelectClearListener(this) + mAdapterDatabaseHistory?.setFileInformationShowListener(this) + databaseFileListView.adapter = mAdapterDatabaseHistory + + // Load default database if not an orientation change + if (!(savedInstanceState != null + && savedInstanceState.containsKey(EXTRA_STAY) + && savedInstanceState.getBoolean(EXTRA_STAY, false))) { + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + val fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "") + + if (fileName!!.isNotEmpty()) { + val dbUri = UriUtil.parseDefaultFile(fileName) + var scheme: String? = null + if (dbUri != null) + scheme = dbUri.scheme + + if (!EmptyUtils.isNullOrEmpty(scheme) && scheme!!.equals("file", ignoreCase = true)) { + val path = dbUri!!.path + val db = File(path!!) + + if (db.exists()) { + launchPasswordActivityWithPath(path) + } + } else { + if (dbUri != null) + launchPasswordActivityWithPath(dbUri.toString()) + } + } + } + + Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) } + } + + private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) { + // If no recent files + if (createButtonView != null + && mFileDatabaseHistory != null + && !mFileDatabaseHistory!!.hasRecentFiles() && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation( + createButtonView!!, + { + openCreateFileDialogFragmentWithPermissionCheck() + }, + { + // But if the user cancel, it can also select a database + performedNextEducation(fileDatabaseSelectActivityEducation) + })) + else if (browseButtonView != null + && fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( + browseButtonView!!, + {tapTargetView -> + mKeyFileHelper?.openFileOnClickViewListener?.onClick(tapTargetView) + }, + { + fileSelectExpandableButtonView?.let { + fileDatabaseSelectActivityEducation + .checkAndPerformedOpenLinkDatabaseEducation(it) + } + } + )) + ; + } + + private fun fileNoFoundAction(e: FileNotFoundException) { + val error = getString(R.string.file_not_found_content) + Toast.makeText(this@FileDatabaseSelectActivity, + error, Toast.LENGTH_LONG).show() + Log.e(TAG, error, e) + } + + private fun launchPasswordActivity(fileName: String, keyFile: String) { + EntrySelectionHelper.doEntrySelectionAction(intent, + { + try { + PasswordActivity.launch(this@FileDatabaseSelectActivity, + fileName, keyFile) + } catch (e: FileNotFoundException) { + fileNoFoundAction(e) + } + }, + { + try { + PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity, + fileName, keyFile) + finish() + } catch (e: FileNotFoundException) { + fileNoFoundAction(e) + } + }, + { assistStructure -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, + fileName, keyFile, + assistStructure) + } catch (e: FileNotFoundException) { + fileNoFoundAction(e) + } + + } + }) + } + + private fun launchPasswordActivityWithPath(path: String) { + launchPasswordActivity(path, "") + // Delete flickering for kitkat <= + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + overridePendingTransition(0, 0) + } + + private fun updateExternalStorageWarning() { + // To show errors + var warning = -1 + val state = Environment.getExternalStorageState() + if (state == Environment.MEDIA_MOUNTED_READ_ONLY) { + warning = R.string.read_only_warning + } else if (state != Environment.MEDIA_MOUNTED) { + warning = R.string.warning_unmounted + } + + val labelWarningView = findViewById(R.id.label_warning) + if (warning != -1) { + labelWarningView.setText(warning) + labelWarningView.visibility = View.VISIBLE + } else { + labelWarningView.visibility = View.INVISIBLE + } + } + + override fun onResume() { + super.onResume() + + updateExternalStorageWarning() + updateFileListVisibility() + mAdapterDatabaseHistory!!.notifyDataSetChanged() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + // only to keep the current activity + outState.putBoolean(EXTRA_STAY, true) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + // NOTE: delegate the permission handling to generated method + onRequestPermissionsResult(requestCode, grantResults) + } + + @NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + fun openCreateFileDialogFragment() { + val createFileDialogFragment = CreateFileDialogFragment() + createFileDialogFragment.show(supportFragmentManager, "createFileDialogFragment") + } + + private fun updateFileListVisibility() { + if (mAdapterDatabaseHistory?.itemCount == 0) + fileListContainer?.visibility = View.INVISIBLE + else + fileListContainer?.visibility = View.VISIBLE + } + + /** + * Create file for database + * @return If not created, return false + */ + private fun createDatabaseFile(path: Uri): Boolean { + + val pathString = URLDecoder.decode(path.path) + // Make sure file name exists + if (pathString.isEmpty()) { + Log.e(TAG, getString(R.string.error_filename_required)) + Toast.makeText(this@FileDatabaseSelectActivity, + R.string.error_filename_required, + Toast.LENGTH_LONG).show() + return false + } + + // Try to create the file + val file = File(pathString) + try { + if (file.exists()) { + Log.e(TAG, getString(R.string.error_database_exists) + " " + file) + Toast.makeText(this@FileDatabaseSelectActivity, + R.string.error_database_exists, + Toast.LENGTH_LONG).show() + return false + } + val parent = file.parentFile + + if (parent == null || parent.exists() && !parent.isDirectory) { + Log.e(TAG, getString(R.string.error_invalid_path) + " " + file) + Toast.makeText(this@FileDatabaseSelectActivity, + R.string.error_invalid_path, + Toast.LENGTH_LONG).show() + return false + } + + if (!parent.exists()) { + // Create parent directory + if (!parent.mkdirs()) { + Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + parent) + Toast.makeText(this@FileDatabaseSelectActivity, + R.string.error_could_not_create_parent, + Toast.LENGTH_LONG).show() + return false + } + } + + return file.createNewFile() + } catch (e: IOException) { + Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + e.localizedMessage) + e.printStackTrace() + Toast.makeText( + this@FileDatabaseSelectActivity, + getText(R.string.error_file_not_create).toString() + " " + + e.localizedMessage, + Toast.LENGTH_LONG).show() + return false + } + + } + + override fun onDefinePathDialogPositiveClick(pathFile: Uri): Boolean { + mDatabaseFileUri = pathFile + return if (createDatabaseFile(pathFile)) { + AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog") + true + } else + false + } + + override fun onDefinePathDialogNegativeClick(pathFile: Uri): Boolean { + return true + } + + override fun onAssignKeyDialogPositiveClick( + masterPasswordChecked: Boolean, masterPassword: String?, + keyFileChecked: Boolean, keyFile: Uri?) { + + try { + mDatabaseFileUri?.path?.let { databaseFilename -> + // Create the new database and start prof + Thread(ProgressDialogRunnable(this, + R.string.progress_create + ) { + CreateDatabaseRunnable(databaseFilename) { database -> + // TODO store database created + AssignPasswordInDatabaseRunnable(this@FileDatabaseSelectActivity, + database, + masterPasswordChecked, + masterPassword, + keyFileChecked, + keyFile, + true, // TODO get readonly + LaunchGroupActivityFinish(UriUtil.parseDefaultFile(databaseFilename)) + ) + } + }).start() + } + } catch (e: Exception) { + val error = "Unable to create database with this password and key file" + Toast.makeText(this, error, Toast.LENGTH_LONG).show() + Log.e(TAG, error + " " + e.message) + // TODO remove + e.printStackTrace() + } + + } + + private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() { + + override fun run() { + finishRun(true, null) + } + + override fun onFinishRun(isSuccess: Boolean, message: String?) { + runOnUiThread { + if (isSuccess) { + // Add database to recent files + mFileDatabaseHistory?.addDatabaseUri(fileURI) + mAdapterDatabaseHistory?.notifyDataSetChanged() + updateFileListVisibility() + GroupActivity.launch(this@FileDatabaseSelectActivity) + } else { + Log.e(TAG, "Unable to open the database") + } + } + } + } + + override fun onAssignKeyDialogNegativeClick( + masterPasswordChecked: Boolean, masterPassword: String?, + keyFileChecked: Boolean, keyFile: Uri?) { + + } + + override fun onFileItemOpenListener(itemPosition: Int) { + OpenFileHistoryAsyncTask({ fileName, keyFile -> + if (fileName != null && keyFile != null) + launchPasswordActivity(fileName, keyFile) + updateFileListVisibility() + }, mFileDatabaseHistory).execute(itemPosition) + } + + override fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel) { + FileInformationDialogFragment.newInstance(fileDatabaseModel).show(supportFragmentManager, "fileInformation") + } + + override fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean { + DeleteFileHistoryAsyncTask({ + mFileDatabaseHistory?.deleteDatabaseUri(fileDatabaseModel.fileUri) + mAdapterDatabaseHistory?.notifyDataSetChanged() + updateFileListVisibility() + }, mFileDatabaseHistory, mAdapterDatabaseHistory).execute(fileDatabaseModel) + return true + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { + super.onActivityResult(requestCode, resultCode, data) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) + } + + mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data + ) { uri -> + if (uri != null) { + if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) { + launchPasswordActivityWithPath(uri.toString()) + } else { + fileSelectExpandableLayout?.expand(false) + openFileNameView?.setText(uri.toString()) + } + } + } + } + + @OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE) + internal fun showRationaleForExternalStorage(request: PermissionRequest) { + AlertDialog.Builder(this) + .setMessage(R.string.permission_external_storage_rationale_write_database) + .setPositiveButton(R.string.allow) { _, _ -> request.proceed() } + .setNegativeButton(R.string.cancel) { _, _ -> request.cancel() } + .show() + } + + @OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE) + internal fun showDeniedForExternalStorage() { + Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show() + } + + @OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE) + internal fun showNeverAskForExternalStorage() { + Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + MenuUtil.defaultMenuInflater(menuInflater, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item) + } + + companion object { + + private const val TAG = "FileDbSelectActivity" + private const val EXTRA_STAY = "EXTRA_STAY" + + /* + * ------------------------- + * No Standard Launch, pass by PasswordActivity + * ------------------------- + */ + + /* + * ------------------------- + * Keyboard Launch + * ------------------------- + */ + + fun launchForKeyboardSelection(activity: Activity) { + KeyboardHelper.startActivityForKeyboardSelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java)) + } + + /* + * ------------------------- + * Autofill Launch + * ------------------------- + */ + + @RequiresApi(api = Build.VERSION_CODES.O) + fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure) { + AutofillHelper.startActivityForAutofillResult(activity, + Intent(activity, FileDatabaseSelectActivity::class.java), + assistStructure) + } + } +} 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 23bd0b8a0..7e8b00b5d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt @@ -22,7 +22,7 @@ import com.kunzisoft.keepass.database.element.GroupVersioned import com.kunzisoft.keepass.database.element.NodeVersioned import com.kunzisoft.keepass.dialogs.SortDialogFragment import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.stylish.StylishFragment +import com.kunzisoft.keepass.activities.stylish.StylishFragment class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt new file mode 100644 index 000000000..bfede2351 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -0,0 +1,895 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.activities + +import android.Manifest +import android.app.Activity +import android.app.assist.AssistStructure +import android.app.backup.BackupManager +import android.content.DialogInterface +import android.content.Intent +import android.content.SharedPreferences +import android.hardware.fingerprint.FingerprintManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.preference.PreferenceManager +import android.support.annotation.RequiresApi +import android.support.v7.app.AlertDialog +import android.support.v7.widget.Toolbar +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.* +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.lock.LockingActivity +import com.kunzisoft.keepass.activities.stylish.StylishActivity +import com.kunzisoft.keepass.activities.utilities.UriIntentInitTask +import com.kunzisoft.keepass.activities.utilities.UriIntentInitTaskCallback +import com.kunzisoft.keepass.app.App +import com.kunzisoft.keepass.autofill.AutofillHelper +import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable +import com.kunzisoft.keepass.database.action.ProgressDialogRunnable +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.dialogs.PasswordEncodingDialogHelper +import com.kunzisoft.keepass.education.PasswordActivityEducation +import com.kunzisoft.keepass.fileselect.KeyFileHelper +import com.kunzisoft.keepass.fingerprint.FingerPrintAnimatedVector +import com.kunzisoft.keepass.fingerprint.FingerPrintExplanationDialog +import com.kunzisoft.keepass.fingerprint.FingerPrintHelper +import com.kunzisoft.keepass.magikeyboard.KeyboardHelper +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.utils.EmptyUtils +import com.kunzisoft.keepass.utils.MenuUtil +import com.kunzisoft.keepass.utils.UriUtil +import permissions.dispatcher.* +import java.io.File +import java.io.FileNotFoundException +import java.lang.ref.WeakReference + +@RuntimePermissions +class PasswordActivity : StylishActivity(), + FingerPrintHelper.FingerPrintCallback, + UriIntentInitTaskCallback { + + // Views + private var toolbar: Toolbar? = null + private var fingerprintContainerView: View? = null + private var fingerPrintAnimatedVector: FingerPrintAnimatedVector? = null + private var fingerprintTextView: TextView? = null + private var fingerprintImageView: ImageView? = null + private var filenameView: TextView? = null + private var passwordView: EditText? = null + private var keyFileView: EditText? = null + private var confirmButtonView: Button? = null + private var checkboxPasswordView: CompoundButton? = null + private var checkboxKeyFileView: CompoundButton? = null + private var checkboxDefaultDatabaseView: CompoundButton? = null + + private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null + + private var mDatabaseFileUri: Uri? = null + private var prefs: SharedPreferences? = null + private var prefsNoBackup: SharedPreferences? = null + + private var mRememberKeyFile: Boolean = false + private var mKeyFileHelper: KeyFileHelper? = null + + private var readOnly: Boolean = false + + private var fingerPrintHelper: FingerPrintHelper? = null + private var fingerprintMustBeConfigured = true + private var fingerPrintMode: FingerPrintHelper.Mode? = null + + // makes it possible to store passwords per database + private val preferenceKeyValue: String + get() = PREF_KEY_VALUE_PREFIX + (mDatabaseFileUri?.path ?: "") + + private val preferenceKeyIvSpec: String + get() = PREF_KEY_IV_PREFIX + (mDatabaseFileUri?.path ?: "") + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + prefs = PreferenceManager.getDefaultSharedPreferences(this) + prefsNoBackup = PreferencesUtil.getNoBackupSharedPreferences(applicationContext) + + mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key), + resources.getBoolean(R.bool.keyfile_default)) + + setContentView(R.layout.password) + + toolbar = findViewById(R.id.toolbar) + toolbar?.title = getString(R.string.app_name) + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + + confirmButtonView = findViewById(R.id.pass_ok) + filenameView = findViewById(R.id.filename) + passwordView = findViewById(R.id.password) + keyFileView = findViewById(R.id.pass_keyfile) + checkboxPasswordView = findViewById(R.id.password_checkbox) + checkboxKeyFileView = findViewById(R.id.keyfile_checkox) + checkboxDefaultDatabaseView = findViewById(R.id.default_database) + + readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) + + val browseView = findViewById(R.id.browse_button) + mKeyFileHelper = KeyFileHelper(this@PasswordActivity) + browseView.setOnClickListener(mKeyFileHelper!!.openFileOnClickViewListener) + + passwordView?.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + + override fun afterTextChanged(editable: Editable) { + if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true) + checkboxPasswordView?.isChecked = true + } + }) + keyFileView?.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + + override fun afterTextChanged(editable: Editable) { + if (editable.toString().isNotEmpty() && checkboxKeyFileView?.isChecked != true) + checkboxKeyFileView?.isChecked = true + } + }) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + fingerprintContainerView = findViewById(R.id.fingerprint_container) + fingerprintTextView = findViewById(R.id.fingerprint_label) + fingerprintImageView = findViewById(R.id.fingerprint_image) + initForFingerprint() + // Init the fingerprint animation + fingerPrintAnimatedVector = FingerPrintAnimatedVector(this, + fingerprintImageView!!) + } + } + + override fun onResume() { + // If the database isn't accessible make sure to clear the password field, if it + // was saved in the instance state + if (App.currentDatabase.loaded) { + setEmptyViews() + } + + // For check shutdown + super.onResume() + + // Enable or not the open button + if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) { + checkboxPasswordView?.let { + confirmButtonView?.isEnabled = it.isChecked + } + } else { + confirmButtonView?.isEnabled = true + } + enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked -> + if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) { + confirmButtonView?.isEnabled = isChecked + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Check if fingerprint well init (be called the first time the fingerprint is configured + // and the activity still active) + if (fingerPrintHelper == null || !fingerPrintHelper!!.isFingerprintInitialized) { + initForFingerprint() + } + + // Start the animation in all cases + fingerPrintAnimatedVector?.startScan() + } else { + checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener) + } + + UriIntentInitTask(WeakReference(this), this, mRememberKeyFile) + .execute(intent) + } + + override fun onSaveInstanceState(outState: Bundle) { + ReadOnlyHelper.onSaveInstanceState(outState, readOnly) + super.onSaveInstanceState(outState) + } + + override fun onPostInitTask(dbUri: Uri?, keyFileUri: Uri?, errorStringId: Int?) { + mDatabaseFileUri = dbUri + + if (errorStringId != null) { + Toast.makeText(this@PasswordActivity, errorStringId, Toast.LENGTH_LONG).show() + finish() + return + } + + // Verify permission to read file + if (mDatabaseFileUri != null && !mDatabaseFileUri!!.scheme!!.contains("content")) + doNothingWithPermissionCheck() + + // Define title + val dbUriString = mDatabaseFileUri?.toString() ?: "" + if (dbUriString.isNotEmpty()) { + if (PreferencesUtil.isFullFilePathEnable(this)) + filenameView?.text = dbUriString + else + filenameView?.text = File(mDatabaseFileUri!!.path!!).name // TODO Encapsulate + } + + // Define Key File text + val keyUriString = keyFileUri?.toString() ?: "" + if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18 + populateKeyFileTextView(keyUriString) + } + + // Define listeners for default database checkbox and validate button + checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked -> + var newDefaultFileName = "" + if (isChecked) { + newDefaultFileName = mDatabaseFileUri?.toString() ?: newDefaultFileName + } + + prefs?.edit()?.apply() { + putString(KEY_DEFAULT_FILENAME, newDefaultFileName) + apply() + } + + val backupManager = BackupManager(this@PasswordActivity) + backupManager.dataChanged() + } + confirmButtonView?.setOnClickListener { verifyAllViewsAndLoadDatabase() } + + // Retrieve settings for default database + val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "") + if (mDatabaseFileUri != null + && !EmptyUtils.isNullOrEmpty(mDatabaseFileUri!!.path) + && UriUtil.equalsDefaultfile(mDatabaseFileUri, defaultFilename)) { + checkboxDefaultDatabaseView?.isChecked = true + } + + // checks if fingerprint is available, will also start listening for fingerprints when available + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + checkFingerprintAvailability() + } + + // If Activity is launch with a password and want to open directly + val intent = intent + val password = intent.getStringExtra(KEY_PASSWORD) + val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false) + if (password != null) { + populatePasswordTextView(password) + } + if (launchImmediately) { + verifyCheckboxesAndLoadDatabase(password, keyFileUri) + } + } + + private fun setEmptyViews() { + populatePasswordTextView(null) + // Bug KeepassDX #18 + if (!mRememberKeyFile) { + populateKeyFileTextView(null) + } + } + + private fun populatePasswordTextView(text: String?) { + if (text == null || text.isEmpty()) { + passwordView?.setText("") + if (checkboxPasswordView?.isChecked == true) + checkboxPasswordView?.isChecked = false + } else { + passwordView?.setText(text) + if (checkboxPasswordView?.isChecked != true) + checkboxPasswordView?.isChecked = true + } + } + + private fun populateKeyFileTextView(text: String?) { + if (text == null || text.isEmpty()) { + keyFileView?.setText("") + if (checkboxKeyFileView?.isChecked == true) + checkboxKeyFileView?.isChecked = false + } else { + keyFileView?.setText(text) + if (checkboxKeyFileView?.isChecked != true) + checkboxKeyFileView?.isChecked = true + } + } + + // fingerprint related code here + @RequiresApi(api = Build.VERSION_CODES.M) + private fun initForFingerprint() { + fingerPrintMode = FingerPrintHelper.Mode.NOT_CONFIGURED_MODE + + fingerPrintHelper = FingerPrintHelper(this, this) + + checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked -> + if (!fingerprintMustBeConfigured) { + // encrypt or decrypt mode based on how much input or not + if (checked) { + toggleFingerprintMode(FingerPrintHelper.Mode.STORE_MODE) + } else { + if (prefsNoBackup?.contains(preferenceKeyValue) == true) { + toggleFingerprintMode(FingerPrintHelper.Mode.OPEN_MODE) + } else { + // This happens when no fingerprints are registered. + toggleFingerprintMode(FingerPrintHelper.Mode.WAITING_PASSWORD_MODE) + } + } + } + + // Add old listener to enable the button, only be call here because of onCheckedChange bug + enableButtonOnCheckedChangeListener?.onCheckedChanged(compoundButton, checked) + } + + // callback for fingerprint findings + fingerPrintHelper?.setAuthenticationCallback(object : FingerprintManager.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence) { + when (errorCode) { + 5 -> Log.i(TAG, "Fingerprint authentication error. Code : $errorCode Error : $errString") + else -> { + Log.e(TAG, "Fingerprint authentication error. Code : $errorCode Error : $errString") + setFingerPrintView(errString.toString(), true) + } + } + } + + override fun onAuthenticationHelp( + helpCode: Int, + helpString: CharSequence) { + Log.w(TAG, "Fingerprint authentication help. Code : $helpCode Help : $helpString") + showError(helpString) + setFingerPrintView(helpString.toString(), true) + fingerprintTextView?.text = helpString + } + + override fun onAuthenticationFailed() { + Log.e(TAG, "Fingerprint authentication failed, fingerprint not recognized") + showError(R.string.fingerprint_not_recognized) + } + + override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) { + when (fingerPrintMode) { + FingerPrintHelper.Mode.STORE_MODE -> { + // newly store the entered password in encrypted way + fingerPrintHelper?.encryptData(passwordView?.text.toString()) + } + FingerPrintHelper.Mode.OPEN_MODE -> { + // retrieve the encrypted value from preferences + prefsNoBackup?.getString(preferenceKeyValue, null)?.let { + fingerPrintHelper?.decryptData(it) + } + } + } + } + }) + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private fun initEncryptData() { + setFingerPrintView(R.string.store_with_fingerprint) + fingerPrintMode = FingerPrintHelper.Mode.STORE_MODE + fingerPrintHelper?.initEncryptData() + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private fun initDecryptData() { + setFingerPrintView(R.string.scanning_fingerprint) + fingerPrintMode = FingerPrintHelper.Mode.OPEN_MODE + if (fingerPrintHelper != null) { + prefsNoBackup?.getString(preferenceKeyIvSpec, null)?.let { + fingerPrintHelper?.initDecryptData(it) + } + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private fun initWaitData() { + setFingerPrintView(R.string.no_password_stored, true) + fingerPrintMode = FingerPrintHelper.Mode.WAITING_PASSWORD_MODE + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Synchronized + private fun toggleFingerprintMode(newMode: FingerPrintHelper.Mode) { + when (newMode) { + FingerPrintHelper.Mode.WAITING_PASSWORD_MODE -> setFingerPrintView(R.string.no_password_stored, true) + FingerPrintHelper.Mode.STORE_MODE -> setFingerPrintView(R.string.store_with_fingerprint) + FingerPrintHelper.Mode.OPEN_MODE -> setFingerPrintView(R.string.scanning_fingerprint) + else -> {} + } + if (newMode != fingerPrintMode) { + fingerPrintMode = newMode + reInitWithFingerprintMode() + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Synchronized + private fun reInitWithFingerprintMode() { + when (fingerPrintMode) { + FingerPrintHelper.Mode.STORE_MODE -> initEncryptData() + FingerPrintHelper.Mode.WAITING_PASSWORD_MODE -> initWaitData() + FingerPrintHelper.Mode.OPEN_MODE -> initDecryptData() + else -> {} + } + // Show fingerprint key deletion + invalidateOptionsMenu() + } + + override fun onPause() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + fingerPrintAnimatedVector?.stopScan() + // stop listening when we go in background + fingerPrintMode = FingerPrintHelper.Mode.NOT_CONFIGURED_MODE + fingerPrintHelper?.stopListening() + } + super.onPause() + } + + private fun setFingerPrintVisibility(vis: Int) { + runOnUiThread { fingerprintContainerView?.visibility = vis } + } + + private fun setFingerPrintView(textId: Int, lock: Boolean = false) { + setFingerPrintView(getString(textId), lock) + } + + private fun setFingerPrintView(text: CharSequence, lock: Boolean) { + runOnUiThread { + fingerprintContainerView?.alpha = if (lock) 0.8f else 1f + fingerprintTextView?.text = text + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Synchronized + private fun checkFingerprintAvailability() { + // fingerprint not supported (by API level or hardware) so keep option hidden + // or manually disable + if (!PreferencesUtil.isFingerprintEnable(applicationContext) + || !FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager::class.java))) { + setFingerPrintVisibility(View.GONE) + } else { + // show explanations + fingerprintContainerView?.setOnClickListener { _ -> + FingerPrintExplanationDialog().show(supportFragmentManager, "fingerprintDialog") + } + setFingerPrintVisibility(View.VISIBLE) + + if (fingerPrintHelper?.hasEnrolledFingerprints() != true) { + // This happens when no fingerprints are registered. Listening won't start + setFingerPrintView(R.string.configure_fingerprint, true) + } else { + fingerprintMustBeConfigured = false + + // fingerprint available but no stored password found yet for this DB so show info don't listen + if (prefsNoBackup?.contains(preferenceKeyValue) != true) { + if (checkboxPasswordView?.isChecked == true) { + // listen for encryption + initEncryptData() + } else { + // wait for typing + initWaitData() + } + } else { + // listen for decryption + initDecryptData() + }// all is set here so we can confirm to user and start listening for fingerprints + }// finally fingerprint available and configured so we can use it + }// fingerprint is available but not configured show icon but in disabled state with some information + + // Show fingerprint key deletion + invalidateOptionsMenu() + } + + private fun removePrefsNoBackupKey() { + prefsNoBackup?.edit() + ?.remove(preferenceKeyValue) + ?.remove(preferenceKeyIvSpec) + ?.apply() + } + + override fun handleEncryptedResult( + value: String, + ivSpec: String) { + prefsNoBackup?.edit() + ?.putString(preferenceKeyValue, value) + ?.putString(preferenceKeyIvSpec, ivSpec) + ?.apply() + verifyAllViewsAndLoadDatabase() + setFingerPrintView(R.string.encrypted_value_stored) + } + + override fun handleDecryptedResult(passwordValue: String) { + // Load database directly + verifyKeyFileViewsAndLoadDatabase(passwordValue) + } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun onInvalidKeyException(e: Exception) { + showError(getString(R.string.fingerprint_invalid_key)) + deleteEntryKey() + } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun onFingerPrintException(e: Exception) { + // Don't show error here; + // showError(getString(R.string.fingerprint_error, e.getMessage())); + // Can be uninit in Activity and init in fragment + setFingerPrintView(e.localizedMessage, true) + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private fun deleteEntryKey() { + fingerPrintHelper?.deleteEntryKey() + removePrefsNoBackupKey() + fingerPrintMode = FingerPrintHelper.Mode.NOT_CONFIGURED_MODE + checkFingerprintAvailability() + } + + private fun showError(messageId: Int) { + showError(getString(messageId)) + } + + private fun showError(message: CharSequence) { + runOnUiThread { Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show() } + } + + private fun verifyAllViewsAndLoadDatabase() { + verifyCheckboxesAndLoadDatabase( + passwordView?.text.toString(), + UriUtil.parseDefaultFile(keyFileView?.text.toString())) + } + + private fun verifyCheckboxesAndLoadDatabase(password: String?, keyFile: Uri?) { + var pass = password + var keyF = keyFile + if (checkboxPasswordView?.isChecked != true) { + pass = null + } + if (checkboxKeyFileView?.isChecked != true) { + keyF = null + } + loadDatabase(pass, keyF) + } + + private fun verifyKeyFileViewsAndLoadDatabase(password: String) { + val key = keyFileView?.text.toString() + var keyUri = UriUtil.parseDefaultFile(key) + if (checkboxKeyFileView?.isChecked != true) { + keyUri = null + } + loadDatabase(password, keyUri) + } + + private fun loadDatabase(password: String?, keyFile: Uri?) { + // Clear before we load + val database = App.currentDatabase + database.closeAndClear(applicationContext) + + mDatabaseFileUri?.let { databaseUri -> + // Show the progress dialog and load the database + Thread(ProgressDialogRunnable( + this, + R.string.loading_database + ) { progressTaskUpdater -> + LoadDatabaseRunnable( + WeakReference(this@PasswordActivity), + database, + databaseUri, + password, + keyFile, + progressTaskUpdater, + AfterLoadingDatabase(database)) + }).start() + } + } + + /** + * Called after verify and try to opening the database + */ + private inner class AfterLoadingDatabase internal constructor(var database: Database) : ActionRunnable() { + + override fun onFinishRun(isSuccess: Boolean, message: String?) { + runOnUiThread { + // Recheck fingerprint if error + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Stay with the same mode + reInitWithFingerprintMode() + } + + if (database.isPasswordEncodingError) { + val dialog = PasswordEncodingDialogHelper() + dialog.show(this@PasswordActivity, + DialogInterface.OnClickListener { _, _ -> launchGroupActivity() }) + } else if (isSuccess) { + launchGroupActivity() + } else { + if (message != null && message.isNotEmpty()) { + Toast.makeText(this@PasswordActivity, message, Toast.LENGTH_LONG).show() + } + } + } + } + } + + private fun launchGroupActivity() { + EntrySelectionHelper.doEntrySelectionAction(intent, + { + GroupActivity.launch(this@PasswordActivity, readOnly) + }, + { + GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly) + // Do not keep history + finish() + }, + { assistStructure -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly) + } + }) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val inflater = menuInflater + // Read menu + inflater.inflate(R.menu.open_file, menu) + changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key)) + + MenuUtil.defaultMenuInflater(inflater, menu) + + // Fingerprint menu + if (!fingerprintMustBeConfigured && prefsNoBackup?.contains(preferenceKeyValue) == true) + inflater.inflate(R.menu.fingerprint, menu) + + super.onCreateOptionsMenu(menu) + + // Show education views + Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) } + + return true + } + + private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation, + menu: Menu) { + if (toolbar != null + && passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation( + toolbar!!, + { + performedNextEducation(passwordActivityEducation, menu) + }, + { + performedNextEducation(passwordActivityEducation, menu) + })) + else if (toolbar != null + && toolbar!!.findViewById(R.id.menu_open_file_read_mode_key) != null + && passwordActivityEducation.checkAndPerformedReadOnlyEducation( + toolbar!!.findViewById(R.id.menu_open_file_read_mode_key), + { + onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key)) + performedNextEducation(passwordActivityEducation, menu) + }, + { + performedNextEducation(passwordActivityEducation, menu) + })) + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && PreferencesUtil.isFingerprintEnable(applicationContext) + && FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager::class.java)) + && passwordActivityEducation.checkAndPerformedFingerprintEducation(fingerprintImageView!!)) + ; + } + + private fun changeOpenFileReadIcon(togglePassword: MenuItem) { + if (readOnly) { + togglePassword.setTitle(R.string.menu_file_selection_read_only) + togglePassword.setIcon(R.drawable.ic_read_only_white_24dp) + } else { + togglePassword.setTitle(R.string.menu_open_file_read_and_write) + togglePassword.setIcon(R.drawable.ic_read_write_white_24dp) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + + when (item.itemId) { + android.R.id.home -> finish() + R.id.menu_open_file_read_mode_key -> { + readOnly = !readOnly + changeOpenFileReadIcon(item) + } + R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + deleteEntryKey() + } + else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) + } + + return super.onOptionsItemSelected(item) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + // NOTE: delegate the permission handling to generated method + onRequestPermissionsResult(requestCode, grantResults) + } + + override fun onActivityResult( + requestCode: Int, + resultCode: Int, + data: Intent) { + super.onActivityResult(requestCode, resultCode, data) + + // To get entry in result + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) + } + + var keyFileResult = false + mKeyFileHelper?.let { + keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data + ) { uri -> + if (uri != null) { + populateKeyFileTextView(uri.toString()) + } + } + } + if (!keyFileResult) { + // this block if not a key file response + when (resultCode) { + LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> { + setEmptyViews() + App.currentDatabase.closeAndClear(applicationContext) + } + } + } + } + + @NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + fun doNothing() { + } + + @OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE) + internal fun showRationaleForExternalStorage(request: PermissionRequest) { + AlertDialog.Builder(this) + .setMessage(R.string.permission_external_storage_rationale_read_database) + .setPositiveButton(R.string.allow) { _, _ -> request.proceed() } + .setNegativeButton(R.string.cancel) { _, _ -> request.cancel() } + .show() + } + + @OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE) + internal fun showDeniedForExternalStorage() { + Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show() + finish() + } + + @OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE) + internal fun showNeverAskForExternalStorage() { + Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show() + finish() + } + + companion object { + + private val TAG = PasswordActivity::class.java.name + + const val KEY_DEFAULT_FILENAME = "defaultFileName" + + private const val KEY_PASSWORD = "password" + private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" + private const val PREF_KEY_VALUE_PREFIX = "valueFor_" // key is a combination of db file name and this prefix + private const val PREF_KEY_IV_PREFIX = "ivFor_" // key is a combination of db file name and this prefix + + private fun buildAndLaunchIntent(activity: Activity, fileName: String, keyFile: String, + intentBuildLauncher: (Intent) -> Unit) { + val intent = Intent(activity, PasswordActivity::class.java) + intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName) + intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile) + intentBuildLauncher.invoke(intent) + } + + @Throws(FileNotFoundException::class) + private fun verifyFileNameUriFromLaunch(fileName: String) { + if (EmptyUtils.isNullOrEmpty(fileName)) { + throw FileNotFoundException() + } + + val uri = UriUtil.parseDefaultFile(fileName) + val scheme = uri.scheme + + if (!EmptyUtils.isNullOrEmpty(scheme) && scheme.equals("file", ignoreCase = true)) { + val dbFile = File(uri.path!!) + if (!dbFile.exists()) { + throw FileNotFoundException() + } + } + } + + /* + * ------------------------- + * Standard Launch + * ------------------------- + */ + + @Throws(FileNotFoundException::class) + fun launch( + activity: Activity, + fileName: String, + keyFile: String) { + verifyFileNameUriFromLaunch(fileName) + buildAndLaunchIntent(activity, fileName, keyFile) { activity.startActivity(it) } + } + + /* + * ------------------------- + * Keyboard Launch + * ------------------------- + */ + + @Throws(FileNotFoundException::class) + fun launchForKeyboardResult( + activity: Activity, + fileName: String, + keyFile: String) { + verifyFileNameUriFromLaunch(fileName) + + buildAndLaunchIntent(activity, fileName, keyFile) { intent -> + KeyboardHelper.startActivityForKeyboardSelection(activity, intent) + } + } + + /* + * ------------------------- + * Autofill Launch + * ------------------------- + */ + + @RequiresApi(api = Build.VERSION_CODES.O) + @Throws(FileNotFoundException::class) + fun launchForAutofillResult( + activity: Activity, + fileName: String, + keyFile: String, + assistStructure: AssistStructure?) { + verifyFileNameUriFromLaunch(fileName) + + if (assistStructure != null) { + buildAndLaunchIntent(activity, fileName, keyFile) { intent -> + AutofillHelper.startActivityForAutofillResult( + activity, + intent, + assistStructure) + } + } else { + launch(activity, fileName, keyFile) + } + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt index dfd61bc85..63b6b8f8f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt @@ -32,7 +32,7 @@ import com.kunzisoft.keepass.activities.EntrySelectionHelper import com.kunzisoft.keepass.activities.ReadOnlyHelper import com.kunzisoft.keepass.app.App import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.stylish.StylishActivity +import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.timeout.TimeoutHelper abstract class LockingActivity : StylishActivity() { diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FilePickerStylishActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/FilePickerStylishActivity.java similarity index 96% rename from app/src/main/java/com/kunzisoft/keepass/fileselect/FilePickerStylishActivity.java rename to app/src/main/java/com/kunzisoft/keepass/activities/stylish/FilePickerStylishActivity.java index 2f5a4569d..afd056c21 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FilePickerStylishActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/FilePickerStylishActivity.java @@ -17,7 +17,7 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.fileselect; +package com.kunzisoft.keepass.activities.stylish; import android.content.Context; import android.os.Bundle; @@ -26,7 +26,6 @@ import android.support.annotation.StyleRes; import android.util.Log; import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.stylish.Stylish; import com.nononsenseapps.filepicker.FilePickerActivity; /** diff --git a/app/src/main/java/com/kunzisoft/keepass/stylish/Stylish.java b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.java similarity index 98% rename from app/src/main/java/com/kunzisoft/keepass/stylish/Stylish.java rename to app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.java index 12e59035e..44bf11e46 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stylish/Stylish.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.java @@ -17,7 +17,7 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.stylish; +package com.kunzisoft.keepass.activities.stylish; import android.content.Context; import android.support.annotation.StyleRes; diff --git a/app/src/main/java/com/kunzisoft/keepass/stylish/StylishActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.java similarity index 96% rename from app/src/main/java/com/kunzisoft/keepass/stylish/StylishActivity.java rename to app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.java index ba193be46..dd8b3dbd1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stylish/StylishActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.java @@ -17,7 +17,7 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.stylish; +package com.kunzisoft.keepass.activities.stylish; import android.os.Bundle; import android.support.annotation.Nullable; diff --git a/app/src/main/java/com/kunzisoft/keepass/stylish/StylishFragment.java b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.java similarity index 98% rename from app/src/main/java/com/kunzisoft/keepass/stylish/StylishFragment.java rename to app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.java index 78824839c..0538ef020 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stylish/StylishFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.java @@ -17,7 +17,7 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.stylish; +package com.kunzisoft.keepass.activities.stylish; import android.content.Context; import android.content.res.TypedArray; diff --git a/app/src/main/java/com/kunzisoft/keepass/password/UriIntentInitTask.kt b/app/src/main/java/com/kunzisoft/keepass/activities/utilities/UriIntentInitTask.kt similarity index 94% rename from app/src/main/java/com/kunzisoft/keepass/password/UriIntentInitTask.kt rename to app/src/main/java/com/kunzisoft/keepass/activities/utilities/UriIntentInitTask.kt index c82b8a500..a7a8c4957 100644 --- a/app/src/main/java/com/kunzisoft/keepass/password/UriIntentInitTask.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/utilities/UriIntentInitTask.kt @@ -1,4 +1,4 @@ -package com.kunzisoft.keepass.password +package com.kunzisoft.keepass.activities.utilities import android.content.Context import android.content.Intent @@ -13,7 +13,7 @@ import com.kunzisoft.keepass.utils.UriUtil import java.io.File import java.lang.ref.WeakReference -internal class UriIntentInitTask(private val weakContext: WeakReference, +class UriIntentInitTask(private val weakContext: WeakReference, private val uriIntentInitTaskCallback: UriIntentInitTaskCallback, private val isKeyFileNeeded: Boolean) : AsyncTask() { @@ -36,7 +36,7 @@ internal class UriIntentInitTask(private val weakContext: WeakReference } else if (incoming.scheme == "file") { val fileName = incoming.path - if (fileName!!.isEmpty()) { + if (fileName?.isNotEmpty() == true) { // No file name return R.string.file_not_found } diff --git a/app/src/main/java/com/kunzisoft/keepass/password/UriIntentInitTaskCallback.java b/app/src/main/java/com/kunzisoft/keepass/activities/utilities/UriIntentInitTaskCallback.kt similarity index 79% rename from app/src/main/java/com/kunzisoft/keepass/password/UriIntentInitTaskCallback.java rename to app/src/main/java/com/kunzisoft/keepass/activities/utilities/UriIntentInitTaskCallback.kt index 07f11e59e..016556d05 100644 --- a/app/src/main/java/com/kunzisoft/keepass/password/UriIntentInitTaskCallback.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/utilities/UriIntentInitTaskCallback.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 Jeremy Jamet / Kunzisoft. + * Copyright 2019 Jeremy Jamet / Kunzisoft. * * This file is part of KeePass DX. * @@ -17,10 +17,10 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.password; +package com.kunzisoft.keepass.activities.utilities -import android.net.Uri; +import android.net.Uri interface UriIntentInitTaskCallback { - void onPostInitTask(Uri dbUri, Uri keyFileUri, Integer errorStringId); + fun onPostInitTask(dbUri: Uri?, keyFileUri: Uri?, errorStringId: Int?) } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt index 555504223..eb5de13ef 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt @@ -39,7 +39,8 @@ class NodeAdapter * Create node list adapter with contextMenu or not * @param context Context to use */ -(private val context: Context, private val menuInflater: MenuInflater) : RecyclerView.Adapter() { +(private val context: Context, private val menuInflater: MenuInflater) + : RecyclerView.Adapter() { private val nodeSortedList: SortedList private val inflater: LayoutInflater = LayoutInflater.from(context) @@ -208,12 +209,11 @@ class NodeAdapter // Assign text holder.text?.text = subNode.title // Assign click - holder.container?.setOnClickListener( - OnNodeClickListener(subNode)) + holder.container?.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) } // Context menu if (activateContextMenu) { holder.container?.setOnCreateContextMenuListener( - ContextMenuBuilder(subNode, nodeMenuListener, readOnly)) + ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener)) } // Add username @@ -279,20 +279,15 @@ class NodeAdapter fun onDeleteMenuClick(node: NodeVersioned): Boolean } - /** - * Utility class for node listener - */ - private inner class OnNodeClickListener internal constructor(private val node: NodeVersioned) : View.OnClickListener { - - override fun onClick(v: View) { - nodeClickCallback?.onNodeClick(node) - } - } - /** * Utility class for menu listener */ - private inner class ContextMenuBuilder internal constructor(private val node: NodeVersioned, private val menuListener: NodeMenuListener?, private val readOnly: Boolean) : View.OnCreateContextMenuListener { + private class ContextMenuBuilder(val menuInflater: MenuInflater, + val node: NodeVersioned, + val readOnly: Boolean, + val isASearchResult: Boolean, + val menuListener: NodeMenuListener?) + : View.OnCreateContextMenuListener { private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item -> if (menuListener == null) @@ -307,21 +302,23 @@ class NodeAdapter } } - override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo) { + override fun onCreateContextMenu(contextMenu: ContextMenu?, + view: View?, + contextMenuInfo: ContextMenu.ContextMenuInfo?) { menuInflater.inflate(R.menu.node_menu, contextMenu) // Opening - var menuItem = contextMenu.findItem(R.id.menu_open) - menuItem.setOnMenuItemClickListener(mOnMyActionClickListener) + var menuItem = contextMenu?.findItem(R.id.menu_open) + menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener) val database = App.currentDatabase // Edition if (readOnly || node == database.recycleBin) { - contextMenu.removeItem(R.id.menu_edit) + contextMenu?.removeItem(R.id.menu_edit) } else { - menuItem = contextMenu.findItem(R.id.menu_edit) - menuItem.setOnMenuItemClickListener(mOnMyActionClickListener) + menuItem = contextMenu?.findItem(R.id.menu_edit) + menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener) } // Copy (not for group) @@ -330,28 +327,28 @@ class NodeAdapter || node == database.recycleBin || node.type == Type.GROUP) { // TODO COPY For Group - contextMenu.removeItem(R.id.menu_copy) + contextMenu?.removeItem(R.id.menu_copy) } else { - menuItem = contextMenu.findItem(R.id.menu_copy) - menuItem.setOnMenuItemClickListener(mOnMyActionClickListener) + menuItem = contextMenu?.findItem(R.id.menu_copy) + menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener) } // Move if (readOnly || isASearchResult || node == database.recycleBin) { - contextMenu.removeItem(R.id.menu_move) + contextMenu?.removeItem(R.id.menu_move) } else { - menuItem = contextMenu.findItem(R.id.menu_move) - menuItem.setOnMenuItemClickListener(mOnMyActionClickListener) + menuItem = contextMenu?.findItem(R.id.menu_move) + menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener) } // Deletion if (readOnly || node == database.recycleBin) { - contextMenu.removeItem(R.id.menu_delete) + contextMenu?.removeItem(R.id.menu_delete) } else { - menuItem = contextMenu.findItem(R.id.menu_delete) - menuItem.setOnMenuItemClickListener(mOnMyActionClickListener) + menuItem = contextMenu?.findItem(R.id.menu_delete) + menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/app/App.kt b/app/src/main/java/com/kunzisoft/keepass/app/App.kt index 0709203a7..c7bfa7ade 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/App.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/App.kt @@ -22,7 +22,7 @@ package com.kunzisoft.keepass.app import android.support.multidex.MultiDexApplication import com.kunzisoft.keepass.compat.PRNGFixes import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.stylish.Stylish +import com.kunzisoft.keepass.activities.stylish.Stylish class App : MultiDexApplication() { diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutoFillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutoFillLauncherActivity.kt index b107d413c..7e248aa4a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutoFillLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutoFillLauncherActivity.kt @@ -30,7 +30,7 @@ import android.support.annotation.RequiresApi import android.support.v7.app.AppCompatActivity import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.app.App -import com.kunzisoft.keepass.fileselect.FileDatabaseSelectActivity +import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/CreateFileDialogFragment.java b/app/src/main/java/com/kunzisoft/keepass/dialogs/CreateFileDialogFragment.java index 99c24ec32..36e241faf 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/CreateFileDialogFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/dialogs/CreateFileDialogFragment.java @@ -44,7 +44,7 @@ import android.widget.Spinner; import android.widget.TextView; import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.fileselect.FilePickerStylishActivity; +import com.kunzisoft.keepass.activities.stylish.FilePickerStylishActivity; import com.kunzisoft.keepass.utils.UriUtil; import com.nononsenseapps.filepicker.FilePickerActivity; import com.nononsenseapps.filepicker.Utils; diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java b/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java index b7a753ca0..3ded5e21f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java @@ -40,7 +40,7 @@ import com.kunzisoft.keepass.R; import com.kunzisoft.keepass.database.element.PwIconStandard; import com.kunzisoft.keepass.icons.IconPack; import com.kunzisoft.keepass.icons.IconPackChooser; -import com.kunzisoft.keepass.stylish.StylishActivity; +import com.kunzisoft.keepass.activities.stylish.StylishActivity; public class IconPickerDialogFragment extends DialogFragment { diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.java deleted file mode 100644 index 3d4ffad56..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2018 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.fileselect; - -import android.os.AsyncTask; - -import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory; - -class DeleteFileHistoryAsyncTask extends AsyncTask { - - private AfterDeleteFileHistoryListener afterDeleteFileHistoryListener; - private FileDatabaseHistory fileHistory; - private FileDatabaseHistoryAdapter adapter; - - DeleteFileHistoryAsyncTask(AfterDeleteFileHistoryListener afterDeleteFileHistoryListener, FileDatabaseHistory fileHistory, FileDatabaseHistoryAdapter adapter) { - this.afterDeleteFileHistoryListener = afterDeleteFileHistoryListener; - this.fileHistory = fileHistory; - this.adapter = adapter; - } - - protected Void doInBackground(FileDatabaseModel... args) { - fileHistory.deleteDatabaseUri(args[0].getFileUri()); - return null; - } - - protected void onPostExecute(Void v) { - adapter.notifyDataSetChanged(); - if (adapter.getItemCount() == 0) { - if(afterDeleteFileHistoryListener != null) - afterDeleteFileHistoryListener.afterDeleteFile(); - } - } - - public interface AfterDeleteFileHistoryListener { - void afterDeleteFile(); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.kt b/app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.kt new file mode 100644 index 000000000..107723c2c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/DeleteFileHistoryAsyncTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.fileselect + +import android.os.AsyncTask + +import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory + +class DeleteFileHistoryAsyncTask(private val afterDeleteFileHistoryListener: (() -> Unit)?, + private val fileHistory: FileDatabaseHistory?, + private val adapter: FileDatabaseHistoryAdapter?) + : AsyncTask() { + + override fun doInBackground(vararg args: FileDatabaseModel): Void? { + fileHistory?.deleteDatabaseUri(args[0].fileUri) + return null + } + + override fun onPostExecute(v: Void) { + adapter?.notifyDataSetChanged() + if (adapter == null || adapter.itemCount == 0) { + afterDeleteFileHistoryListener?.invoke() + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.java deleted file mode 100644 index 4c69b2cbc..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2018 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.fileselect; - -import android.content.Context; -import android.content.res.Resources; -import android.net.Uri; -import android.support.annotation.ColorInt; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; -import android.util.TypedValue; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.settings.PreferencesUtil; - -import java.util.List; - -public class FileDatabaseHistoryAdapter extends RecyclerView.Adapter { - - private static final int MENU_CLEAR = 1; - - private Context context; - private LayoutInflater inflater; - private List listFiles; - private FileItemOpenListener fileItemOpenListener; - private FileSelectClearListener fileSelectClearListener; - private FileInformationShowListener fileInformationShowListener; - - private @ColorInt - int defaultColor; - private @ColorInt - int warningColor; - - FileDatabaseHistoryAdapter(Context context, List listFiles) { - this.inflater = LayoutInflater.from(context); - this.context = context; - this.listFiles = listFiles; - - TypedValue typedValue = new TypedValue(); - Resources.Theme theme = context.getTheme(); - theme.resolveAttribute(R.attr.colorAccentCompat, typedValue, true); - warningColor = typedValue.data; - theme.resolveAttribute(android.R.attr.textColorHintInverse, typedValue, true); - defaultColor = typedValue.data; - } - - @NonNull - @Override - public FileDatabaseHistoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = inflater.inflate(R.layout.file_row, parent, false); - return new FileDatabaseHistoryViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull FileDatabaseHistoryViewHolder holder, int position) { - FileDatabaseModel fileDatabaseModel = new FileDatabaseModel(context, listFiles.get(position)); - // Context menu creation - holder.getFileContainer().setOnCreateContextMenuListener(new ContextMenuBuilder(fileDatabaseModel)); - // Click item to open file - if (fileItemOpenListener != null) - holder.getFileContainer().setOnClickListener(new FileItemClickListener(position)); - // Assign file name - if (PreferencesUtil.isFullFilePathEnable(context)) - holder.getFileName().setText(Uri.decode(fileDatabaseModel.getFileUri().toString())); - else - holder.getFileName().setText(fileDatabaseModel.getFileName()); - holder.getFileName().setTextSize(PreferencesUtil.getListTextSize(context)); - // Click on information - if (fileInformationShowListener != null) - holder.getFileInformation().setOnClickListener(new FileInformationClickListener(fileDatabaseModel)); - } - - @Override - public int getItemCount() { - return listFiles.size(); - } - - void setOnItemClickListener(FileItemOpenListener fileItemOpenListener) { - this.fileItemOpenListener = fileItemOpenListener; - } - - void setFileSelectClearListener(FileSelectClearListener fileSelectClearListener) { - this.fileSelectClearListener = fileSelectClearListener; - } - - void setFileInformationShowListener(FileInformationShowListener fileInformationShowListener) { - this.fileInformationShowListener = fileInformationShowListener; - } - - public interface FileItemOpenListener { - void onFileItemOpenListener(int itemPosition); - } - - public interface FileSelectClearListener { - boolean onFileSelectClearListener(FileDatabaseModel fileDatabaseModel); - } - - public interface FileInformationShowListener { - void onClickFileInformation(FileDatabaseModel fileDatabaseModel); - } - - private class FileItemClickListener implements View.OnClickListener { - - private int position; - - FileItemClickListener(int position) { - this.position = position; - } - - @Override - public void onClick(View v) { - fileItemOpenListener.onFileItemOpenListener(position); - } - } - - private class FileInformationClickListener implements View.OnClickListener { - - private FileDatabaseModel fileDatabaseModel; - - FileInformationClickListener(FileDatabaseModel fileDatabaseModel) { - this.fileDatabaseModel = fileDatabaseModel; - } - - @Override - public void onClick(View view) { - fileInformationShowListener.onClickFileInformation(fileDatabaseModel); - } - } - - private class ContextMenuBuilder implements View.OnCreateContextMenuListener { - - private FileDatabaseModel fileDatabaseModel; - - ContextMenuBuilder(FileDatabaseModel fileDatabaseModel) { - this.fileDatabaseModel = fileDatabaseModel; - } - - @Override - public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) { - MenuItem clearMenu = contextMenu.add(Menu.NONE, MENU_CLEAR, Menu.NONE, R.string.remove_from_filelist); - clearMenu.setOnMenuItemClickListener(mOnMyActionClickListener); - } - - private MenuItem.OnMenuItemClickListener mOnMyActionClickListener = new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - if (fileSelectClearListener == null) - return false; - switch ( item.getItemId() ) { - case MENU_CLEAR: - return fileSelectClearListener.onFileSelectClearListener(fileDatabaseModel); - default: - return false; - } - } - }; - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.kt new file mode 100644 index 000000000..c2e89f4f0 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.fileselect + +import android.content.Context +import android.content.res.Resources +import android.net.Uri +import android.support.annotation.ColorInt +import android.support.v7.widget.RecyclerView +import android.util.TypedValue +import android.view.ContextMenu +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView + +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.settings.PreferencesUtil + +class FileDatabaseHistoryAdapter(private val context: Context, private val listFiles: List) + : RecyclerView.Adapter() { + + private val inflater: LayoutInflater = LayoutInflater.from(context) + private var fileItemOpenListener: FileItemOpenListener? = null + private var fileSelectClearListener: FileSelectClearListener? = null + private var fileInformationShowListener: FileInformationShowListener? = null + + @ColorInt + private val defaultColor: Int + @ColorInt + private val warningColor: Int + + init { + + val typedValue = TypedValue() + val theme = context.theme + theme.resolveAttribute(R.attr.colorAccentCompat, typedValue, true) + warningColor = typedValue.data + theme.resolveAttribute(android.R.attr.textColorHintInverse, typedValue, true) + defaultColor = typedValue.data + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder { + val view = inflater.inflate(R.layout.file_row, parent, false) + return FileDatabaseHistoryViewHolder(view) + } + + override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) { + val fileDatabaseModel = FileDatabaseModel(context, listFiles[position]) + // Context menu creation + holder.fileContainer.setOnCreateContextMenuListener(ContextMenuBuilder(fileDatabaseModel)) + // Click item to open file + if (fileItemOpenListener != null) + holder.fileContainer.setOnClickListener(FileItemClickListener(position)) + // Assign file name + if (PreferencesUtil.isFullFilePathEnable(context)) + holder.fileName.text = Uri.decode(fileDatabaseModel.fileUri.toString()) + else + holder.fileName.text = fileDatabaseModel.fileName + holder.fileName.textSize = PreferencesUtil.getListTextSize(context) + // Click on information + if (fileInformationShowListener != null) + holder.fileInformation.setOnClickListener(FileInformationClickListener(fileDatabaseModel)) + } + + override fun getItemCount(): Int { + return listFiles.size + } + + fun setOnItemClickListener(fileItemOpenListener: FileItemOpenListener) { + this.fileItemOpenListener = fileItemOpenListener + } + + fun setFileSelectClearListener(fileSelectClearListener: FileSelectClearListener) { + this.fileSelectClearListener = fileSelectClearListener + } + + fun setFileInformationShowListener(fileInformationShowListener: FileInformationShowListener) { + this.fileInformationShowListener = fileInformationShowListener + } + + interface FileItemOpenListener { + fun onFileItemOpenListener(itemPosition: Int) + } + + interface FileSelectClearListener { + fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean + } + + interface FileInformationShowListener { + fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel) + } + + private inner class FileItemClickListener(private val position: Int) : View.OnClickListener { + + override fun onClick(v: View) { + fileItemOpenListener?.onFileItemOpenListener(position) + } + } + + private inner class FileInformationClickListener(private val fileDatabaseModel: FileDatabaseModel) : View.OnClickListener { + + override fun onClick(view: View) { + fileInformationShowListener?.onClickFileInformation(fileDatabaseModel) + } + } + + private inner class ContextMenuBuilder(private val fileDatabaseModel: FileDatabaseModel) : View.OnCreateContextMenuListener { + + private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item -> + if (fileSelectClearListener == null) + return@OnMenuItemClickListener false + when (item.itemId) { + MENU_CLEAR -> fileSelectClearListener!!.onFileSelectClearListener(fileDatabaseModel) + else -> false + } + } + + override fun onCreateContextMenu(contextMenu: ContextMenu?, view: View?, contextMenuInfo: ContextMenu.ContextMenuInfo?) { + contextMenu?.add(Menu.NONE, MENU_CLEAR, Menu.NONE, R.string.remove_from_filelist) + ?.setOnMenuItemClickListener(mOnMyActionClickListener) + } + } + + inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + var fileContainer: View = itemView.findViewById(R.id.file_container) + var fileName: TextView = itemView.findViewById(R.id.file_filename) + var fileInformation: ImageView = itemView.findViewById(R.id.file_information) + } + + companion object { + + private const val MENU_CLEAR = 1 + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseModel.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseModel.java index 07de0acd8..79c64e1a5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseModel.java +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseModel.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Jeremy Jamet / Kunzisoft. + * Copyright 2019 Jeremy Jamet / Kunzisoft. * * This file is part of KeePass DX. * diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java deleted file mode 100644 index 345a357dd..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseSelectActivity.java +++ /dev/null @@ -1,602 +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.fileselect; - -import android.Manifest; -import android.app.Activity; -import android.app.assist.AssistStructure; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.RequiresApi; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.Toolbar; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; - -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.activities.EntrySelectionHelper; -import com.kunzisoft.keepass.activities.GroupActivity; -import com.kunzisoft.keepass.autofill.AutofillHelper; -import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable; -import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable; -import com.kunzisoft.keepass.database.action.ProgressDialogRunnable; -import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment; -import com.kunzisoft.keepass.dialogs.CreateFileDialogFragment; -import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation; -import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory; -import com.kunzisoft.keepass.magikeyboard.KeyboardHelper; -import com.kunzisoft.keepass.password.PasswordActivity; -import com.kunzisoft.keepass.settings.PreferencesUtil; -import com.kunzisoft.keepass.stylish.StylishActivity; -import com.kunzisoft.keepass.tasks.ActionRunnable; -import com.kunzisoft.keepass.utils.EmptyUtils; -import com.kunzisoft.keepass.utils.MenuUtil; -import com.kunzisoft.keepass.utils.UriUtil; - -import net.cachapa.expandablelayout.ExpandableLayout; - -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.net.URLDecoder; - -import permissions.dispatcher.NeedsPermission; -import permissions.dispatcher.OnNeverAskAgain; -import permissions.dispatcher.OnPermissionDenied; -import permissions.dispatcher.OnShowRationale; -import permissions.dispatcher.PermissionRequest; -import permissions.dispatcher.RuntimePermissions; - -@RuntimePermissions -public class FileDatabaseSelectActivity extends StylishActivity implements - CreateFileDialogFragment.DefinePathDialogListener, - AssignMasterKeyDialogFragment.AssignPasswordDialogListener, - FileDatabaseHistoryAdapter.FileItemOpenListener, - FileDatabaseHistoryAdapter.FileSelectClearListener, - FileDatabaseHistoryAdapter.FileInformationShowListener { - - private static final String TAG = "FileDbSelectActivity"; - - private static final String EXTRA_STAY = "EXTRA_STAY"; - - private FileDatabaseHistoryAdapter mAdapter; - private View fileListContainer; - private View createButtonView; - private View browseButtonView; - private View openButtonView; - - private FileDatabaseHistory fileDatabaseHistory; - - private View fileSelectExpandableButton; - private ExpandableLayout fileSelectExpandable; - private EditText openFileNameView; - - private Uri databaseUri; - - private KeyFileHelper keyFileHelper; - - private String defaultPath; - - /* - * ------------------------- - * No Standard Launch, pass by PasswordActivity - * ------------------------- - */ - - /* - * ------------------------- - * Keyboard Launch - * ------------------------- - */ - - public static void launchForKeyboardSelection(Activity activity) { - KeyboardHelper.INSTANCE.startActivityForKeyboardSelection(activity, new Intent(activity, FileDatabaseSelectActivity.class)); - } - - /* - * ------------------------- - * Autofill Launch - * ------------------------- - */ - - @RequiresApi(api = Build.VERSION_CODES.O) - public static void launchForAutofillResult(Activity activity, @NonNull AssistStructure assistStructure) { - AutofillHelper.INSTANCE.startActivityForAutofillResult(activity, - new Intent(activity, FileDatabaseSelectActivity.class), - assistStructure); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - fileDatabaseHistory = FileDatabaseHistory.Companion.getInstance(new WeakReference<>(getApplicationContext())); - - setContentView(R.layout.file_selection); - fileListContainer = findViewById(R.id.container_file_list); - - Toolbar toolbar = findViewById(R.id.toolbar); - toolbar.setTitle(""); - setSupportActionBar(toolbar); - - openFileNameView = findViewById(R.id.file_filename); - - // Set the initial value of the filename - defaultPath = Environment.getExternalStorageDirectory().getAbsolutePath() - + getString(R.string.database_file_path_default) - + getString(R.string.database_file_name_default) - + getString(R.string.database_file_extension_default); - openFileNameView.setHint(R.string.open_link_database); - - // Button to expand file selection - fileSelectExpandableButton = findViewById(R.id.file_select_expandable_button); - fileSelectExpandable = findViewById(R.id.file_select_expandable); - fileSelectExpandableButton.setOnClickListener(view -> { - if (fileSelectExpandable.isExpanded()) - fileSelectExpandable.collapse(); - else - fileSelectExpandable.expand(); - }); - - // History list - RecyclerView mListFiles = findViewById(R.id.file_list); - mListFiles.setLayoutManager(new LinearLayoutManager(this)); - - // Open button - openButtonView = findViewById(R.id.open_database); - openButtonView.setOnClickListener(v -> { - String fileName = openFileNameView.getText().toString(); - if (fileName.isEmpty()) - fileName = defaultPath; - launchPasswordActivityWithPath(fileName); - }); - - // Create button - createButtonView = findViewById(R.id.create_database); - createButtonView .setOnClickListener(v -> - FileDatabaseSelectActivityPermissionsDispatcher - .openCreateFileDialogFragmentWithPermissionCheck(FileDatabaseSelectActivity.this) - ); - - keyFileHelper = new KeyFileHelper(this); - browseButtonView = findViewById(R.id.browse_button); - browseButtonView.setOnClickListener(keyFileHelper.getOpenFileOnClickViewListener( - () -> Uri.parse("file://" + openFileNameView.getText().toString()))); - - // Construct adapter with listeners - mAdapter = new FileDatabaseHistoryAdapter(FileDatabaseSelectActivity.this, fileDatabaseHistory.getDatabaseUriList()); - mAdapter.setOnItemClickListener(this); - mAdapter.setFileSelectClearListener(this); - mAdapter.setFileInformationShowListener(this); - mListFiles.setAdapter(mAdapter); - - // Load default database if not an orientation change - if (! (savedInstanceState != null - && savedInstanceState.containsKey(EXTRA_STAY) - && savedInstanceState.getBoolean(EXTRA_STAY, false)) ) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - String fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, ""); - - if (fileName.length() > 0) { - Uri dbUri = UriUtil.parseDefaultFile(fileName); - String scheme = null; - if (dbUri != null) - scheme = dbUri.getScheme(); - - if (!EmptyUtils.INSTANCE.isNullOrEmpty(scheme) && scheme.equalsIgnoreCase("file")) { - String path = dbUri.getPath(); - File db = new File(path); - - if (db.exists()) { - launchPasswordActivityWithPath(path); - } - } else { - if (dbUri != null) - launchPasswordActivityWithPath(dbUri.toString()); - } - } - } - - new Handler().post(() -> performedNextEducation(new FileDatabaseSelectActivityEducation(this))); - } - - private void performedNextEducation(FileDatabaseSelectActivityEducation fileDatabaseSelectActivityEducation) { - // If no recent files - if (!fileDatabaseHistory.hasRecentFiles() - && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation( - createButtonView, - tapTargetView -> { - FileDatabaseSelectActivityPermissionsDispatcher - .openCreateFileDialogFragmentWithPermissionCheck(FileDatabaseSelectActivity.this); - return null; - }, - tapTargetView -> { - // But if the user cancel, it can also select a database - performedNextEducation(fileDatabaseSelectActivityEducation); - return null; - }) - ); - else if (fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( - browseButtonView, - tapTargetView -> { - keyFileHelper.getOpenFileOnClickViewListener().onClick(tapTargetView); - return null; - }, - tapTargetView -> { - fileDatabaseSelectActivityEducation.checkAndPerformedOpenLinkDatabaseEducation( - fileSelectExpandableButton, - tapTargetView1 -> null, - tapTargetView12 -> null - ); - return null; - } - )); - } - - private void fileNoFoundAction(FileNotFoundException e) { - String error = getString(R.string.file_not_found_content); - Toast.makeText(FileDatabaseSelectActivity.this, - error, Toast.LENGTH_LONG).show(); - Log.e(TAG, error, e); - } - - private void launchPasswordActivity(String fileName, String keyFile) { - EntrySelectionHelper.INSTANCE.doEntrySelectionAction(getIntent(), - () -> { - try { - PasswordActivity.launch(FileDatabaseSelectActivity.this, - fileName, keyFile); - } catch (FileNotFoundException e) { - fileNoFoundAction(e); - } - return null; - }, - () -> { - try { - PasswordActivity.launchForKeyboardResult(FileDatabaseSelectActivity.this, - fileName, keyFile); - finish(); - } catch (FileNotFoundException e) { - fileNoFoundAction(e); - } - return null; - }, - assistStructure -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - PasswordActivity.launchForAutofillResult(FileDatabaseSelectActivity.this, - fileName, keyFile, - assistStructure); - } catch (FileNotFoundException e) { - fileNoFoundAction(e); - } - } - return null; - }); - } - - private void launchPasswordActivityWithPath(String path) { - launchPasswordActivity(path, ""); - // Delete flickering for kitkat <= - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) - overridePendingTransition(0, 0); - } - - private void updateExternalStorageWarning() { - // To show errors - int warning = -1; - String state = Environment.getExternalStorageState(); - if (state.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { - warning = R.string.read_only_warning; - } else if (!state.equals(Environment.MEDIA_MOUNTED)) { - warning = R.string.warning_unmounted; - } - - TextView labelWarningView = findViewById(R.id.label_warning); - if (warning != -1) { - labelWarningView.setText(warning); - labelWarningView.setVisibility(View.VISIBLE); - } else { - labelWarningView.setVisibility(View.INVISIBLE); - } - } - - @Override - protected void onResume() { - super.onResume(); - - updateExternalStorageWarning(); - updateFileListVisibility(); - mAdapter.notifyDataSetChanged(); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - // only to keep the current activity - outState.putBoolean(EXTRA_STAY, true); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - // NOTE: delegate the permission handling to generated method - FileDatabaseSelectActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults); - } - - @NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - public void openCreateFileDialogFragment() { - CreateFileDialogFragment createFileDialogFragment = new CreateFileDialogFragment(); - createFileDialogFragment.show(getSupportFragmentManager(), "createFileDialogFragment"); - } - - private void updateFileListVisibility() { - if(mAdapter.getItemCount() == 0) - fileListContainer.setVisibility(View.INVISIBLE); - else - fileListContainer.setVisibility(View.VISIBLE); - } - - /** - * Create file for database - * @return If not created, return false - */ - private boolean createDatabaseFile(Uri path) { - - String pathString = URLDecoder.decode(path.getPath()); - // Make sure file name exists - if (pathString.length() == 0) { - Log.e(TAG, getString(R.string.error_filename_required)); - Toast.makeText(FileDatabaseSelectActivity.this, - R.string.error_filename_required, - Toast.LENGTH_LONG).show(); - return false; - } - - // Try to create the file - File file = new File(pathString); - try { - if (file.exists()) { - Log.e(TAG, getString(R.string.error_database_exists) + " " + file); - Toast.makeText(FileDatabaseSelectActivity.this, - R.string.error_database_exists, - Toast.LENGTH_LONG).show(); - return false; - } - File parent = file.getParentFile(); - - if ( parent == null || (parent.exists() && ! parent.isDirectory()) ) { - Log.e(TAG, getString(R.string.error_invalid_path) + " " + file); - Toast.makeText(FileDatabaseSelectActivity.this, - 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(FileDatabaseSelectActivity.this, - R.string.error_could_not_create_parent, - Toast.LENGTH_LONG).show(); - return false; - } - } - - return file.createNewFile(); - } catch (IOException e) { - Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + e.getLocalizedMessage()); - e.printStackTrace(); - Toast.makeText( - FileDatabaseSelectActivity.this, - getText(R.string.error_file_not_create) + " " - + e.getLocalizedMessage(), - Toast.LENGTH_LONG).show(); - return false; - } - } - - @Override - public boolean onDefinePathDialogPositiveClick(Uri pathFile) { - databaseUri = pathFile; - if(createDatabaseFile(pathFile)) { - AssignMasterKeyDialogFragment assignMasterKeyDialogFragment = new AssignMasterKeyDialogFragment(); - assignMasterKeyDialogFragment.show(getSupportFragmentManager(), "passwordDialog"); - return true; - } else - return false; - } - - @Override - public boolean onDefinePathDialogNegativeClick(Uri pathFile) { - return true; - } - - @Override - public void onAssignKeyDialogPositiveClick( - boolean masterPasswordChecked, String masterPassword, - boolean keyFileChecked, Uri keyFile) { - - try { - String databaseFilename = databaseUri.getPath(); - - if (databaseFilename != null) { - // Create the new database and start prof - new Thread(new ProgressDialogRunnable(this, - R.string.progress_create, - progressTaskUpdater -> - new CreateDatabaseRunnable(databaseFilename, database -> { - // TODO store database created - return new AssignPasswordInDatabaseRunnable(FileDatabaseSelectActivity.this, - database, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile, - true, // TODO get readonly - new LaunchGroupActivityFinish(UriUtil.parseDefaultFile(databaseFilename)) - ); - }) - )).start(); - } - } catch (Exception e) { - String error = "Unable to create database with this password and key file"; - Toast.makeText(this, error, Toast.LENGTH_LONG).show(); - Log.e(TAG, error + " " + e.getMessage()); - // TODO remove - e.printStackTrace(); - } - } - - private class LaunchGroupActivityFinish extends ActionRunnable { - - private Uri fileURI; - - LaunchGroupActivityFinish(Uri fileUri) { - super(); - this.fileURI = fileUri; - } - - @Override - public void run() { - finishRun(true, null); - } - - @Override - public void onFinishRun(boolean isSuccess, @Nullable String message) { - runOnUiThread(() -> { - if (isSuccess) { - // Add database to recent files - fileDatabaseHistory.addDatabaseUri(fileURI); - mAdapter.notifyDataSetChanged(); - updateFileListVisibility(); - GroupActivity.Companion.launch(FileDatabaseSelectActivity.this); - } else { - Log.e(TAG, "Unable to open the database"); - } - }); - } - } - - @Override - public void onAssignKeyDialogNegativeClick( - boolean masterPasswordChecked, String masterPassword, - boolean keyFileChecked, Uri keyFile) { - - } - - @Override - public void onFileItemOpenListener(int itemPosition) { - new OpenFileHistoryAsyncTask((fileName, keyFile) -> { - launchPasswordActivity(fileName, keyFile); - updateFileListVisibility(); - }, fileDatabaseHistory).execute(itemPosition); - } - - @Override - public void onClickFileInformation(FileDatabaseModel fileDatabaseModel) { - if (fileDatabaseModel != null) { - FileInformationDialogFragment fileInformationDialogFragment = - FileInformationDialogFragment.newInstance(fileDatabaseModel); - fileInformationDialogFragment.show(getSupportFragmentManager(), "fileInformation"); - } - } - - @Override - public boolean onFileSelectClearListener(final FileDatabaseModel fileDatabaseModel) { - new DeleteFileHistoryAsyncTask(() -> { - fileDatabaseHistory.deleteDatabaseUri(fileDatabaseModel.getFileUri()); - mAdapter.notifyDataSetChanged(); - updateFileListVisibility(); - }, fileDatabaseHistory, mAdapter).execute(fileDatabaseModel); - return true; - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.INSTANCE.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data); - } - - keyFileHelper.onActivityResultCallback(requestCode, resultCode, data, - uri -> { - if (uri != null) { - if (PreferencesUtil.autoOpenSelectedFile(FileDatabaseSelectActivity.this)) { - launchPasswordActivityWithPath(uri.toString()); - } else { - fileSelectExpandable.expand(false); - openFileNameView.setText(uri.toString()); - } - } - }); - } - - @OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE) - void showRationaleForExternalStorage(final PermissionRequest request) { - new AlertDialog.Builder(this) - .setMessage(R.string.permission_external_storage_rationale_write_database) - .setPositiveButton(R.string.allow, (dialog, which) -> request.proceed()) - .setNegativeButton(R.string.cancel, (dialog, which) -> request.cancel()) - .show(); - } - - @OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE) - void showDeniedForExternalStorage() { - Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show(); - } - - @OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE) - void showNeverAskForExternalStorage() { - Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - MenuUtil.INSTANCE.defaultMenuInflater(getMenuInflater(), menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - return MenuUtil.INSTANCE.onDefaultMenuOptionsItemSelected(this, item) - && super.onOptionsItemSelected(item); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.java b/app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.java deleted file mode 100644 index 39169eed7..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2018 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.fileselect; - -import android.os.AsyncTask; - -import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory; - -class OpenFileHistoryAsyncTask extends AsyncTask { - - private AfterOpenFileHistoryListener afterOpenFileHistoryListener; - private FileDatabaseHistory fileHistory; - private String fileName; - private String keyFile; - - OpenFileHistoryAsyncTask(AfterOpenFileHistoryListener afterOpenFileHistoryListener, FileDatabaseHistory fileHistory) { - this.afterOpenFileHistoryListener = afterOpenFileHistoryListener; - this.fileHistory = fileHistory; - } - - protected Void doInBackground(Integer... args) { - int position = args[0]; - fileName = fileHistory.getDatabaseAt(position); - keyFile = fileHistory.getKeyFileAt(position); - return null; - } - - protected void onPostExecute(Void v) { - afterOpenFileHistoryListener.afterOpenFile(fileName, keyFile); - } - - public interface AfterOpenFileHistoryListener { - void afterOpenFile(String fileName, String keyFile); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryViewHolder.kt b/app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.kt similarity index 51% rename from app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryViewHolder.kt rename to app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.kt index 622e1ae01..ef77ae268 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryViewHolder.kt +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/OpenFileHistoryAsyncTask.kt @@ -19,16 +19,26 @@ */ package com.kunzisoft.keepass.fileselect -import android.support.v7.widget.RecyclerView -import android.view.View -import android.widget.ImageView -import android.widget.TextView +import android.os.AsyncTask -import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory -internal class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { +class OpenFileHistoryAsyncTask(private val afterOpenFileHistoryListener: ((fileName: String?, keyFile: String?) -> Unit)?, + private val fileHistory: FileDatabaseHistory?) + : AsyncTask() { - var fileContainer: View = itemView.findViewById(R.id.file_container) - var fileName: TextView = itemView.findViewById(R.id.file_filename) - var fileInformation: ImageView = itemView.findViewById(R.id.file_information) + private var fileName: String? = null + private var keyFile: String? = null + + override fun doInBackground(vararg args: Int?): Void? { + args[0]?.let { + fileName = fileHistory?.getDatabaseAt(it) + keyFile = fileHistory?.getKeyFileAt(it) + } + return null + } + + override fun onPostExecute(v: Void?) { + afterOpenFileHistoryListener?.invoke(fileName, keyFile) + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistory.kt b/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistory.kt index b6126e53d..97607ef4d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistory.kt +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/database/FileDatabaseHistory.kt @@ -38,6 +38,7 @@ class FileDatabaseHistory private constructor(private val context: WeakReference private val mKeyFilesUriList = ArrayList() private val mPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.get()) + var isEnabled: Boolean = false val databaseUriList: List @@ -170,7 +171,9 @@ class FileDatabaseHistory private constructor(private val context: WeakReference list.clear() for (i in 0 until size) { - list.add(mPreferences.getString(keyPrefix + "_" + i, "")) + mPreferences.getString(keyPrefix + "_" + i, "")?.let { + list.add(it) + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt index 875509217..0a9329d2c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt @@ -4,7 +4,7 @@ import android.os.Bundle import android.support.v7.app.AppCompatActivity import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.app.App -import com.kunzisoft.keepass.fileselect.FileDatabaseSelectActivity +import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationCopyingService.java b/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationCopyingService.java index 469b2a57c..3f1bb91e7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationCopyingService.java +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationCopyingService.java @@ -36,7 +36,7 @@ import android.util.TypedValue; import com.kunzisoft.keepass.R; import com.kunzisoft.keepass.database.exception.SamsungClipboardException; -import com.kunzisoft.keepass.stylish.Stylish; +import com.kunzisoft.keepass.activities.stylish.Stylish; import com.kunzisoft.keepass.timeout.ClipboardHelper; import java.util.ArrayList; diff --git a/app/src/main/java/com/kunzisoft/keepass/password/IntentBuildLauncher.java b/app/src/main/java/com/kunzisoft/keepass/password/IntentBuildLauncher.java deleted file mode 100644 index aa69659d1..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/password/IntentBuildLauncher.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.kunzisoft.keepass.password; - -import android.content.Intent; - -public interface IntentBuildLauncher { - void launchActivity(Intent intent); -} diff --git a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java deleted file mode 100644 index 5b7a941c3..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java +++ /dev/null @@ -1,998 +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.password; - -import android.Manifest; -import android.app.Activity; -import android.app.assist.AssistStructure; -import android.app.backup.BackupManager; -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.NonNull; -import android.support.annotation.RequiresApi; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.Toolbar; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.Button; -import android.widget.CompoundButton; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.activities.EntrySelectionHelper; -import com.kunzisoft.keepass.activities.GroupActivity; -import com.kunzisoft.keepass.activities.ReadOnlyHelper; -import com.kunzisoft.keepass.activities.lock.LockingActivity; -import com.kunzisoft.keepass.app.App; -import com.kunzisoft.keepass.autofill.AutofillHelper; -import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable; -import com.kunzisoft.keepass.database.action.ProgressDialogRunnable; -import com.kunzisoft.keepass.database.element.Database; -import com.kunzisoft.keepass.dialogs.PasswordEncodingDialogHelper; -import com.kunzisoft.keepass.education.PasswordActivityEducation; -import com.kunzisoft.keepass.fileselect.KeyFileHelper; -import com.kunzisoft.keepass.fingerprint.FingerPrintAnimatedVector; -import com.kunzisoft.keepass.fingerprint.FingerPrintExplanationDialog; -import com.kunzisoft.keepass.fingerprint.FingerPrintHelper; -import com.kunzisoft.keepass.magikeyboard.KeyboardHelper; -import com.kunzisoft.keepass.settings.PreferencesUtil; -import com.kunzisoft.keepass.stylish.StylishActivity; -import com.kunzisoft.keepass.tasks.ActionRunnable; -import com.kunzisoft.keepass.utils.EmptyUtils; -import com.kunzisoft.keepass.utils.MenuUtil; -import com.kunzisoft.keepass.utils.UriUtil; - -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.FileNotFoundException; -import java.lang.ref.WeakReference; - -import permissions.dispatcher.NeedsPermission; -import permissions.dispatcher.OnNeverAskAgain; -import permissions.dispatcher.OnPermissionDenied; -import permissions.dispatcher.OnShowRationale; -import permissions.dispatcher.PermissionRequest; -import permissions.dispatcher.RuntimePermissions; - -@RuntimePermissions -public class PasswordActivity extends StylishActivity - implements FingerPrintHelper.FingerPrintCallback, UriIntentInitTaskCallback { - - private static final String TAG = PasswordActivity.class.getName(); - - public static final String KEY_DEFAULT_FILENAME = "defaultFileName"; - - private static final String KEY_PASSWORD = "password"; - private static final String KEY_LAUNCH_IMMEDIATELY = "launchImmediately"; - - private Uri mDbUri = null; - SharedPreferences prefs; - SharedPreferences prefsNoBackup; - - private FingerPrintHelper fingerPrintHelper; - private boolean fingerprintMustBeConfigured = true; - private boolean mRememberKeyfile; - - private FingerPrintHelper.Mode fingerPrintMode; - private static final String PREF_KEY_VALUE_PREFIX = "valueFor_"; // key is a combination of db file name and this prefix - private static final String PREF_KEY_IV_PREFIX = "ivFor_"; // key is a combination of db file name and this prefix - - private Toolbar toolbar; - private View fingerprintContainerView; - private FingerPrintAnimatedVector fingerPrintAnimatedVector; - private TextView fingerprintTextView; - private ImageView fingerprintImageView; - private TextView filenameView; - private EditText passwordView; - private EditText keyFileView; - private Button confirmButtonView; - private CompoundButton checkboxPasswordView; - private CompoundButton checkboxKeyfileView; - private CompoundButton checkboxDefaultDatabaseView; - private CompoundButton.OnCheckedChangeListener enableButtonOncheckedChangeListener; - - private boolean readOnly; - - private DefaultCheckChange defaultCheckChange; - private ValidateButtonViewClickListener validateButtonViewClickListener; - - private KeyFileHelper keyFileHelper; - - private static void buildAndLaunchIntent(Activity activity, String fileName, String keyFile, - IntentBuildLauncher intentBuildLauncher) { - Intent intent = new Intent(activity, PasswordActivity.class); - intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName); - intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile); - intentBuildLauncher.launchActivity(intent); - } - - private static void verifyFileNameUriFromLaunch(String fileName) throws FileNotFoundException { - if (EmptyUtils.INSTANCE.isNullOrEmpty(fileName)) { - throw new FileNotFoundException(); - } - - Uri uri = UriUtil.parseDefaultFile(fileName); - assert uri != null; - String scheme = uri.getScheme(); - - if (!EmptyUtils.INSTANCE.isNullOrEmpty(scheme) && scheme.equalsIgnoreCase("file")) { - File dbFile = new File(uri.getPath()); - if (!dbFile.exists()) { - throw new FileNotFoundException(); - } - } - } - - /* - * ------------------------- - * Standard Launch - * ------------------------- - */ - - public static void launch( - Activity activity, - String fileName, - String keyFile) throws FileNotFoundException { - verifyFileNameUriFromLaunch(fileName); - buildAndLaunchIntent(activity, fileName, keyFile, activity::startActivity); - } - - /* - * ------------------------- - * Keyboard Launch - * ------------------------- - */ - - public static void launchForKeyboardResult( - Activity activity, - String fileName, - String keyFile) throws FileNotFoundException { - verifyFileNameUriFromLaunch(fileName); - - buildAndLaunchIntent(activity, fileName, keyFile, (intent) -> { - KeyboardHelper.INSTANCE.startActivityForKeyboardSelection(activity, intent); - }); - } - - /* - * ------------------------- - * Autofill Launch - * ------------------------- - */ - - @RequiresApi(api = Build.VERSION_CODES.O) - public static void launchForAutofillResult( - Activity activity, - String fileName, - String keyFile, - AssistStructure assistStructure) throws FileNotFoundException { - verifyFileNameUriFromLaunch(fileName); - - if ( assistStructure != null ) { - buildAndLaunchIntent(activity, fileName, keyFile, (intent) -> { - AutofillHelper.INSTANCE.startActivityForAutofillResult( - activity, - intent, - assistStructure); - }); - } else { - launch(activity, fileName, keyFile); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - prefs = PreferenceManager.getDefaultSharedPreferences(this); - prefsNoBackup = PreferencesUtil.getNoBackupSharedPreferences(getApplicationContext()); - - mRememberKeyfile = prefs.getBoolean(getString(R.string.keyfile_key), - getResources().getBoolean(R.bool.keyfile_default)); - - setContentView(R.layout.password); - - toolbar = findViewById(R.id.toolbar); - toolbar.setTitle(getString(R.string.app_name)); - setSupportActionBar(toolbar); - assert getSupportActionBar() != null; - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setDisplayShowHomeEnabled(true); - - confirmButtonView = findViewById(R.id.pass_ok); - filenameView = findViewById(R.id.filename); - passwordView = findViewById(R.id.password); - keyFileView = findViewById(R.id.pass_keyfile); - checkboxPasswordView = findViewById(R.id.password_checkbox); - checkboxKeyfileView = findViewById(R.id.keyfile_checkox); - checkboxDefaultDatabaseView = findViewById(R.id.default_database); - - readOnly = ReadOnlyHelper.INSTANCE.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState); - - View browseView = findViewById(R.id.browse_button); - keyFileHelper = new KeyFileHelper(PasswordActivity.this); - browseView.setOnClickListener(keyFileHelper.getOpenFileOnClickViewListener()); - - passwordView.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} - - @Override - public void afterTextChanged(Editable editable) { - if (!editable.toString().isEmpty() && !checkboxPasswordView.isChecked()) - checkboxPasswordView.setChecked(true); - } - }); - keyFileView.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} - - @Override - public void afterTextChanged(Editable editable) { - if (!editable.toString().isEmpty() && !checkboxKeyfileView.isChecked()) - checkboxKeyfileView.setChecked(true); - } - }); - - defaultCheckChange = new DefaultCheckChange(); - validateButtonViewClickListener = new ValidateButtonViewClickListener(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - fingerprintContainerView = findViewById(R.id.fingerprint_container); - fingerprintTextView = findViewById(R.id.fingerprint_label); - fingerprintImageView = findViewById(R.id.fingerprint_image); - initForFingerprint(); - // Init the fingerprint animation - fingerPrintAnimatedVector = new FingerPrintAnimatedVector(this, - fingerprintImageView); - } - } - - @Override - protected void onResume() { - // If the database isn't accessible make sure to clear the password field, if it - // was saved in the instance state - if (App.Companion.getCurrentDatabase().getLoaded()) { - setEmptyViews(); - } - - // For check shutdown - super.onResume(); - - // Enable or not the open button - if (!PreferencesUtil.emptyPasswordAllowed(PasswordActivity.this)) { - confirmButtonView.setEnabled(checkboxPasswordView.isChecked()); - } else { - confirmButtonView.setEnabled(true); - } - enableButtonOncheckedChangeListener = (buttonView, isChecked) -> { - if (!PreferencesUtil.emptyPasswordAllowed(PasswordActivity.this)) { - confirmButtonView.setEnabled(isChecked); - } - }; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // Check if fingerprint well init (be called the first time the fingerprint is configured - // and the activity still active) - if (fingerPrintHelper == null || !fingerPrintHelper.isFingerprintInitialized()) { - initForFingerprint(); - } - - // Start the animation in all cases - if (fingerPrintAnimatedVector != null) { - fingerPrintAnimatedVector.startScan(); - } - } else { - checkboxPasswordView.setOnCheckedChangeListener(enableButtonOncheckedChangeListener); - } - - new UriIntentInitTask(new WeakReference<>(this), this, mRememberKeyfile) - .execute(getIntent()); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - ReadOnlyHelper.INSTANCE.onSaveInstanceState(outState, readOnly); - super.onSaveInstanceState(outState); - } - - @Override - public void onPostInitTask(Uri dbUri, Uri keyFileUri, Integer errorStringId) { - mDbUri = dbUri; - - if (errorStringId != null) { - Toast.makeText(PasswordActivity.this, errorStringId, Toast.LENGTH_LONG).show(); - finish(); - return; - } - - // Verify permission to read file - if (mDbUri != null - && !dbUri.getScheme().contains("content")) - PasswordActivityPermissionsDispatcher - .doNothingWithPermissionCheck(this); - - // Define title - String dbUriString = (mDbUri == null) ? "" : mDbUri.toString(); - if (!dbUriString.isEmpty()) { - if (PreferencesUtil.isFullFilePathEnable(this)) - filenameView.setText(dbUriString); - else - filenameView.setText(new File(mDbUri.getPath()).getName()); // TODO Encapsulate - } - - // Define Key File text - String keyUriString = (keyFileUri == null) ? "" : keyFileUri.toString(); - if (!keyUriString.isEmpty() && mRememberKeyfile) { // Bug KeepassDX #18 - populateKeyFileTextView(keyUriString); - } - - // Define listeners for default database checkbox and validate button - checkboxDefaultDatabaseView.setOnCheckedChangeListener(defaultCheckChange); - confirmButtonView.setOnClickListener(validateButtonViewClickListener); - - // Retrieve settings for default database - String defaultFilename = prefs.getString(KEY_DEFAULT_FILENAME, ""); - if (mDbUri!=null - && !EmptyUtils.INSTANCE.isNullOrEmpty(mDbUri.getPath()) - && UriUtil.equalsDefaultfile(mDbUri, defaultFilename)) { - checkboxDefaultDatabaseView.setChecked(true); - } - - // checks if fingerprint is available, will also start listening for fingerprints when available - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - checkFingerprintAvailability(); - } - - // If Activity is launch with a password and want to open directly - Intent intent = getIntent(); - String password = intent.getStringExtra(KEY_PASSWORD); - boolean launch_immediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false); - if (password != null) { - populatePasswordTextView(password); - } - if (launch_immediately) { - verifyCheckboxesAndLoadDatabase(password, keyFileUri); - } - } - - private void setEmptyViews() { - populatePasswordTextView(null); - // Bug KeepassDX #18 - if (!mRememberKeyfile) { - populateKeyFileTextView(null); - } - } - - private void populatePasswordTextView(String text) { - if (text == null || text.isEmpty()) { - passwordView.setText(""); - if (checkboxPasswordView.isChecked()) - checkboxPasswordView.setChecked(false); - } else { - passwordView.setText(text); - if (!checkboxPasswordView.isChecked()) - checkboxPasswordView.setChecked(true); - } - } - - private void populateKeyFileTextView(String text) { - if (text == null || text.isEmpty()) { - keyFileView.setText(""); - if (checkboxKeyfileView.isChecked()) - checkboxKeyfileView.setChecked(false); - } else { - keyFileView.setText(text); - if (!checkboxKeyfileView.isChecked()) - checkboxKeyfileView.setChecked(true); - } - } - - // fingerprint related code here - @RequiresApi(api = Build.VERSION_CODES.M) - private void initForFingerprint() { - fingerPrintMode = FingerPrintHelper.Mode.NOT_CONFIGURED_MODE; - - fingerPrintHelper = new FingerPrintHelper(this, this); - - checkboxPasswordView.setOnCheckedChangeListener((compoundButton, checked) -> { - if ( !fingerprintMustBeConfigured ) { - // encrypt or decrypt mode based on how much input or not - if (checked) { - toggleFingerprintMode(FingerPrintHelper.Mode.STORE_MODE); - } else { - if (!prefsNoBackup.contains(getPreferenceKeyValue())) { - // This happens when no fingerprints are registered. - toggleFingerprintMode(FingerPrintHelper.Mode.WAITING_PASSWORD_MODE); - } else { - toggleFingerprintMode(FingerPrintHelper.Mode.OPEN_MODE); - } - } - } - - // Add old listener to enable the button, only be call here because of onCheckedChange bug - if (enableButtonOncheckedChangeListener != null) - enableButtonOncheckedChangeListener.onCheckedChanged(compoundButton, checked); - }); - - // callback for fingerprint findings - fingerPrintHelper.setAuthenticationCallback(new FingerprintManager.AuthenticationCallback() { - @Override - public void onAuthenticationError( - final int errorCode, - final CharSequence errString) { - switch (errorCode) { - case 5: - Log.i(TAG, "Fingerprint authentication error. Code : " + errorCode + " Error : " + errString); - break; - default: - Log.e(TAG, "Fingerprint authentication error. Code : " + errorCode + " Error : " + errString); - setFingerPrintView(errString.toString(), true); - } - } - - @Override - public void onAuthenticationHelp( - final int helpCode, - final CharSequence helpString) { - Log.w(TAG, "Fingerprint authentication help. Code : " + helpCode + " Help : " + helpString); - showError(helpString); - setFingerPrintView(helpString.toString(), true); - fingerprintTextView.setText(helpString); - } - - @Override - public void onAuthenticationFailed() { - Log.e(TAG, "Fingerprint authentication failed, fingerprint not recognized"); - showError(R.string.fingerprint_not_recognized); - } - - @Override - public void onAuthenticationSucceeded(final FingerprintManager.AuthenticationResult result) { - switch (fingerPrintMode) { - case STORE_MODE: - // newly store the entered password in encrypted way - final String password = passwordView.getText().toString(); - fingerPrintHelper.encryptData(password); - break; - case OPEN_MODE: - // retrieve the encrypted value from preferences - final String encryptedValue = prefsNoBackup.getString(getPreferenceKeyValue(), null); - if (encryptedValue != null) { - fingerPrintHelper.decryptData(encryptedValue); - } - break; - } - } - }); - } - - private String getPreferenceKeyValue() { - // makes it possible to store passwords uniqly per database - return PREF_KEY_VALUE_PREFIX + (mDbUri != null ? mDbUri.getPath() : ""); - } - - private String getPreferenceKeyIvSpec() { - return PREF_KEY_IV_PREFIX + (mDbUri != null ? mDbUri.getPath() : ""); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private void initEncryptData() { - setFingerPrintView(R.string.store_with_fingerprint); - fingerPrintMode = FingerPrintHelper.Mode.STORE_MODE; - if (fingerPrintHelper != null) - fingerPrintHelper.initEncryptData(); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private void initDecryptData() { - setFingerPrintView(R.string.scanning_fingerprint); - fingerPrintMode = FingerPrintHelper.Mode.OPEN_MODE; - if (fingerPrintHelper != null) { - final String ivSpecValue = prefsNoBackup.getString(getPreferenceKeyIvSpec(), null); - if (ivSpecValue != null) - fingerPrintHelper.initDecryptData(ivSpecValue); - } - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private void initWaitData() { - setFingerPrintView(R.string.no_password_stored, true); - fingerPrintMode = FingerPrintHelper.Mode.WAITING_PASSWORD_MODE; - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private synchronized void toggleFingerprintMode(final FingerPrintHelper.Mode newMode) { - switch (newMode) { - case WAITING_PASSWORD_MODE: - setFingerPrintView(R.string.no_password_stored, true); - break; - case STORE_MODE: - setFingerPrintView(R.string.store_with_fingerprint); - break; - case OPEN_MODE: - setFingerPrintView(R.string.scanning_fingerprint); - break; - } - if( !newMode.equals(fingerPrintMode) ) { - fingerPrintMode = newMode; - reInitWithFingerprintMode(); - } - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private synchronized void reInitWithFingerprintMode() { - switch (fingerPrintMode) { - case STORE_MODE: - initEncryptData(); - break; - case WAITING_PASSWORD_MODE: - initWaitData(); - break; - case OPEN_MODE: - initDecryptData(); - break; - } - // Show fingerprint key deletion - invalidateOptionsMenu(); - } - - @Override - protected void onPause() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (fingerPrintAnimatedVector != null) { - fingerPrintAnimatedVector.stopScan(); - } - // stop listening when we go in background - fingerPrintMode = FingerPrintHelper.Mode.NOT_CONFIGURED_MODE; - if (fingerPrintHelper != null) { - fingerPrintHelper.stopListening(); - } - } - super.onPause(); - } - - private void setFingerPrintVisibility(final int vis) { - runOnUiThread(() -> fingerprintContainerView.setVisibility(vis)); - } - - private void setFingerPrintView(final int textId) { - setFingerPrintView(textId, false); - } - - private void setFingerPrintView(final int textId, boolean lock) { - setFingerPrintView(getString(textId), lock); - } - - private void setFingerPrintView(final CharSequence text, boolean lock) { - runOnUiThread(() -> { - if (lock) { - fingerprintContainerView.setAlpha(0.8f); - } else - fingerprintContainerView.setAlpha(1f); - fingerprintTextView.setText(text); - }); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private synchronized void checkFingerprintAvailability() { - // fingerprint not supported (by API level or hardware) so keep option hidden - // or manually disable - if (!PreferencesUtil .isFingerprintEnable(getApplicationContext()) - || !FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager.class))) { - setFingerPrintVisibility(View.GONE); - } - // fingerprint is available but not configured show icon but in disabled state with some information - else { - // show explanations - fingerprintContainerView.setOnClickListener(view -> { - FingerPrintExplanationDialog fingerPrintDialog = new FingerPrintExplanationDialog(); - fingerPrintDialog.show(getSupportFragmentManager(), "fingerprintDialog"); - }); - setFingerPrintVisibility(View.VISIBLE); - - if (!fingerPrintHelper.hasEnrolledFingerprints()) { - // This happens when no fingerprints are registered. Listening won't start - setFingerPrintView(R.string.configure_fingerprint, true); - } - // finally fingerprint available and configured so we can use it - else { - fingerprintMustBeConfigured = false; - - // fingerprint available but no stored password found yet for this DB so show info don't listen - if (!prefsNoBackup.contains(getPreferenceKeyValue())) { - if (checkboxPasswordView.isChecked()) { - // listen for encryption - initEncryptData(); - } else { - // wait for typing - initWaitData(); - } - } - // all is set here so we can confirm to user and start listening for fingerprints - else { - // listen for decryption - initDecryptData(); - } - } - } - - // Show fingerprint key deletion - invalidateOptionsMenu(); - } - - private void removePrefsNoBackupKey() { - prefsNoBackup.edit() - .remove(getPreferenceKeyValue()) - .remove(getPreferenceKeyIvSpec()) - .apply(); - } - - @Override - public void handleEncryptedResult( - final String value, - final String ivSpec) { - prefsNoBackup.edit() - .putString(getPreferenceKeyValue(), value) - .putString(getPreferenceKeyIvSpec(), ivSpec) - .apply(); - verifyAllViewsAndLoadDatabase(); - setFingerPrintView(R.string.encrypted_value_stored); - } - - @Override - public void handleDecryptedResult(final String passwordValue) { - // Load database directly - verifyKeyFileViewsAndLoadDatabase(passwordValue); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - @Override - public void onInvalidKeyException(Exception e) { - showError(getString(R.string.fingerprint_invalid_key)); - deleteEntryKey(); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - @Override - public void onFingerPrintException(Exception e) { - // Don't show error here; - // showError(getString(R.string.fingerprint_error, e.getMessage())); - // Can be uninit in Activity and init in fragment - setFingerPrintView(e.getLocalizedMessage(), true); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private void deleteEntryKey() { - fingerPrintHelper.deleteEntryKey(); - removePrefsNoBackupKey(); - fingerPrintMode = FingerPrintHelper.Mode.NOT_CONFIGURED_MODE; - checkFingerprintAvailability(); - } - - private void showError(final int messageId) { - showError(getString(messageId)); - } - - private void showError(final CharSequence message) { - runOnUiThread(() -> Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show()); - } - - private class DefaultCheckChange implements CompoundButton.OnCheckedChangeListener { - @Override - public void onCheckedChanged( - CompoundButton buttonView, - boolean isChecked) { - - String newDefaultFileName = ""; - if (isChecked) { - newDefaultFileName = mDbUri.toString(); - } - - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(KEY_DEFAULT_FILENAME, newDefaultFileName); - editor.apply(); - - BackupManager backupManager = new BackupManager(PasswordActivity.this); - backupManager.dataChanged(); - } - } - - private class ValidateButtonViewClickListener implements View.OnClickListener { - public void onClick(View view) { - verifyAllViewsAndLoadDatabase(); - } - } - - private void verifyAllViewsAndLoadDatabase() { - String pass = passwordView.getText().toString(); - String keyfile = keyFileView.getText().toString(); - verifyCheckboxesAndLoadDatabase(pass, UriUtil.parseDefaultFile(keyfile)); - } - - private void verifyCheckboxesAndLoadDatabase(String pass, Uri keyfile) { - if (!checkboxPasswordView.isChecked()) { - pass = null; - } - if (!checkboxKeyfileView.isChecked()) { - keyfile = null; - } - loadDatabase(pass, keyfile); - } - - private void verifyKeyFileViewsAndLoadDatabase(String password) { - String key = keyFileView.getText().toString(); - Uri keyUri = UriUtil.parseDefaultFile(key); - if (!checkboxKeyfileView.isChecked()) { - keyUri = null; - } - loadDatabase(password, keyUri); - } - - private void loadDatabase(String password, Uri keyfile) { - // Clear before we load - Database database = App.Companion.getCurrentDatabase(); - database.closeAndClear(getApplicationContext()); - - // Show the progress dialog and load the database - new Thread(new ProgressDialogRunnable( - this, - R.string.loading_database, - progressTaskUpdater -> new LoadDatabaseRunnable( - new WeakReference<>(PasswordActivity.this), - database, - mDbUri, - password, - keyfile, - progressTaskUpdater, - new AfterLoadingDatabase(database)) - )).start(); - } - - /** - * Called after verify and try to opening the database - */ - private final class AfterLoadingDatabase extends ActionRunnable { - - protected Database database; - - AfterLoadingDatabase(Database database) { - super(); - this.database = database; - } - - @Override - public void onFinishRun(boolean isSuccess, @Nullable String message) { - runOnUiThread(() -> { - // Recheck fingerprint if error - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // Stay with the same mode - reInitWithFingerprintMode(); - } - - if (database.isPasswordEncodingError()) { - PasswordEncodingDialogHelper dialog = new PasswordEncodingDialogHelper(); - dialog.show(PasswordActivity.this, (dialog1, which) -> launchGroupActivity()); - } else if (isSuccess) { - launchGroupActivity(); - } else { - if ( getMessage() != null && getMessage().length() > 0 ) { - Toast.makeText(PasswordActivity.this, getMessage(), Toast.LENGTH_LONG).show(); - } - } - }); - } - } - - private void launchGroupActivity() { - EntrySelectionHelper.INSTANCE.doEntrySelectionAction(getIntent(), - () -> { - GroupActivity.Companion.launch(PasswordActivity.this, readOnly); - return null; - }, - () -> { - GroupActivity.Companion.launchForKeyboardSelection(PasswordActivity.this, readOnly); - // Do not keep history - finish(); - return null; - }, - assistStructure -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - GroupActivity.Companion.launchForAutofillResult(PasswordActivity.this, assistStructure, readOnly); - } - return null; - }); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - // Read menu - inflater.inflate(R.menu.open_file, menu); - changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key)); - - MenuUtil.INSTANCE.defaultMenuInflater(inflater, menu); - - // Fingerprint menu - if (!fingerprintMustBeConfigured - && prefsNoBackup.contains(getPreferenceKeyValue()) ) - inflater.inflate(R.menu.fingerprint, menu); - - super.onCreateOptionsMenu(menu); - - // Show education views - new Handler().post(() -> performedNextEducation(new PasswordActivityEducation(this), menu)); - - return true; - } - - private void performedNextEducation(PasswordActivityEducation passwordActivityEducation, - Menu menu) { - if (passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation( - toolbar, - tapTargetView -> { - performedNextEducation(passwordActivityEducation, menu); - return null; - }, - tapTargetView -> { - performedNextEducation(passwordActivityEducation, menu); - return null; - }) - ); - else if (toolbar.findViewById(R.id.menu_open_file_read_mode_key) != null - && passwordActivityEducation.checkAndPerformedReadOnlyEducation( - toolbar.findViewById(R.id.menu_open_file_read_mode_key), - tapTargetView -> { - onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key)); - performedNextEducation(passwordActivityEducation, menu); - return null; - }, - tapTargetView -> { - performedNextEducation(passwordActivityEducation, menu); - return null; - }) - ); - else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && PreferencesUtil.isFingerprintEnable(getApplicationContext()) - && FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager.class)) - && passwordActivityEducation.checkAndPerformedFingerprintEducation(fingerprintImageView, - tapTargetView -> null, - tapTargetView -> null) - ); - } - - private void changeOpenFileReadIcon(MenuItem togglePassword) { - if ( readOnly ) { - togglePassword.setTitle(R.string.menu_file_selection_read_only); - togglePassword.setIcon(R.drawable.ic_read_only_white_24dp); - } else { - togglePassword.setTitle(R.string.menu_open_file_read_and_write); - togglePassword.setIcon(R.drawable.ic_read_write_white_24dp); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - switch (item.getItemId()) { - case android.R.id.home: - finish(); - break; - case R.id.menu_open_file_read_mode_key: - readOnly = !readOnly; - changeOpenFileReadIcon(item); - break; - case R.id.menu_fingerprint_remove_key: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - deleteEntryKey(); - } - break; - default: - return MenuUtil.INSTANCE.onDefaultMenuOptionsItemSelected(this, item); - } - - return super.onOptionsItemSelected(item); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - // NOTE: delegate the permission handling to generated method - PasswordActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults); - } - - @Override - protected void onActivityResult( - int requestCode, - int resultCode, - Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - // To get entry in result - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.INSTANCE.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data); - } - - boolean keyFileResult = false; - if (keyFileHelper != null) { - keyFileResult = keyFileHelper.onActivityResultCallback(requestCode, resultCode, data, - uri -> { - if (uri != null) { - populateKeyFileTextView(uri.toString()); - } - }); - } - if (!keyFileResult) { - // this block if not a key file response - switch (resultCode) { - case LockingActivity.RESULT_EXIT_LOCK: - case Activity.RESULT_CANCELED: - setEmptyViews(); - App.Companion.getCurrentDatabase().closeAndClear(getApplicationContext()); - break; - } - } - } - - @NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - public void doNothing() {} - - @OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE) - void showRationaleForExternalStorage(final PermissionRequest request) { - new AlertDialog.Builder(this) - .setMessage(R.string.permission_external_storage_rationale_read_database) - .setPositiveButton(R.string.allow, (dialog, which) -> request.proceed()) - .setNegativeButton(R.string.cancel, (dialog, which) -> request.cancel()) - .show(); - } - - @OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE) - void showDeniedForExternalStorage() { - Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show(); - finish(); - } - - @OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE) - void showNeverAskForExternalStorage() { - Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show(); - finish(); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/MagikIMESettings.java b/app/src/main/java/com/kunzisoft/keepass/settings/MagikIMESettings.java index fadc84980..306d10771 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/MagikIMESettings.java +++ b/app/src/main/java/com/kunzisoft/keepass/settings/MagikIMESettings.java @@ -24,7 +24,7 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; import com.kunzisoft.keepass.R; -import com.kunzisoft.keepass.stylish.StylishActivity; +import com.kunzisoft.keepass.activities.stylish.StylishActivity; public class MagikIMESettings extends StylishActivity { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java index 768370de8..908b98047 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java @@ -61,7 +61,7 @@ import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseNamePrefe import com.kunzisoft.keepass.settings.preferencedialogfragment.MemoryUsagePreferenceDialogFragmentCompat; import com.kunzisoft.keepass.settings.preferencedialogfragment.ParallelismPreferenceDialogFragmentCompat; import com.kunzisoft.keepass.settings.preferencedialogfragment.RoundsPreferenceDialogFragmentCompat; -import com.kunzisoft.keepass.stylish.Stylish; +import com.kunzisoft.keepass.activities.stylish.Stylish; import java.lang.ref.WeakReference; diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt index 854602b4e..ec4779cfb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt @@ -30,7 +30,7 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.AboutActivity import com.kunzisoft.keepass.activities.ReadOnlyHelper.READ_ONLY_DEFAULT import com.kunzisoft.keepass.settings.SettingsActivity -import com.kunzisoft.keepass.stylish.StylishActivity +import com.kunzisoft.keepass.activities.stylish.StylishActivity object MenuUtil { From a158e96ba6d27cd2a6b390f144e10d8b4f8cb6a5 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 10 Jul 2019 17:59:59 +0200 Subject: [PATCH 16/24] Refactor BasicViewHolder --- .../keepass/adapters/BasicViewHolder.kt | 33 ------------------ .../keepass/adapters/EntryViewHolder.kt | 34 ------------------- .../keepass/adapters/GroupViewHolder.kt | 34 ------------------- .../kunzisoft/keepass/adapters/NodeAdapter.kt | 33 +++++++++++++++++- 4 files changed, 32 insertions(+), 102 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/adapters/BasicViewHolder.kt delete mode 100644 app/src/main/java/com/kunzisoft/keepass/adapters/EntryViewHolder.kt delete mode 100644 app/src/main/java/com/kunzisoft/keepass/adapters/GroupViewHolder.kt diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/BasicViewHolder.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/BasicViewHolder.kt deleted file mode 100644 index 4d5ab9169..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/BasicViewHolder.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.adapters - -import android.support.v7.widget.RecyclerView -import android.view.View -import android.widget.ImageView -import android.widget.TextView - -abstract class BasicViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - - var container: View? = null - var icon: ImageView? = null - var text: TextView? = null - var subText: TextView? = null -} diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryViewHolder.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryViewHolder.kt deleted file mode 100644 index 7b1ffecc1..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryViewHolder.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.adapters - -import android.view.View - -import com.kunzisoft.keepass.R - -internal class EntryViewHolder(itemView: View) : BasicViewHolder(itemView) { - - init { - container = itemView.findViewById(R.id.entry_container) - icon = itemView.findViewById(R.id.entry_icon) - text = itemView.findViewById(R.id.entry_text) - subText = itemView.findViewById(R.id.entry_subtext) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/GroupViewHolder.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/GroupViewHolder.kt deleted file mode 100644 index b73ca7d91..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/GroupViewHolder.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.adapters - -import android.view.View - -import com.kunzisoft.keepass.R - -internal class GroupViewHolder(itemView: View) : BasicViewHolder(itemView) { - - init { - container = itemView.findViewById(R.id.group_container) - icon = itemView.findViewById(R.id.group_icon) - text = itemView.findViewById(R.id.group_text) - subText = itemView.findViewById(R.id.group_subtext) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt index eb5de13ef..8e497e182 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt @@ -26,6 +26,8 @@ import android.support.v7.widget.RecyclerView import android.support.v7.widget.util.SortedListAdapterCallback import android.util.Log import android.view.* +import android.widget.ImageView +import android.widget.TextView import android.widget.Toast import com.kunzisoft.keepass.R import com.kunzisoft.keepass.app.App @@ -40,7 +42,7 @@ class NodeAdapter * @param context Context to use */ (private val context: Context, private val menuInflater: MenuInflater) - : RecyclerView.Adapter() { + : RecyclerView.Adapter() { private val nodeSortedList: SortedList private val inflater: LayoutInflater = LayoutInflater.from(context) @@ -353,6 +355,35 @@ class NodeAdapter } } + abstract class BasicViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + var container: View? = null + var icon: ImageView? = null + var text: TextView? = null + var subText: TextView? = null + } + + + internal class GroupViewHolder(itemView: View) : BasicViewHolder(itemView) { + + init { + container = itemView.findViewById(R.id.group_container) + icon = itemView.findViewById(R.id.group_icon) + text = itemView.findViewById(R.id.group_text) + subText = itemView.findViewById(R.id.group_subtext) + } + } + + internal class EntryViewHolder(itemView: View) : BasicViewHolder(itemView) { + + init { + container = itemView.findViewById(R.id.entry_container) + icon = itemView.findViewById(R.id.entry_icon) + text = itemView.findViewById(R.id.entry_text) + subText = itemView.findViewById(R.id.entry_subtext) + } + } + companion object { private val TAG = NodeAdapter::class.java.name } From 6a5ed7f460c45b680badf52dd261d1b7abc1e625 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 10 Jul 2019 19:10:59 +0200 Subject: [PATCH 17/24] Fix kotlin null crash --- .../activities/FileDatabaseSelectActivity.kt | 2 +- .../keepass/activities/PasswordActivity.kt | 2 +- .../dialogs/AssignMasterKeyDialogFragment.kt | 117 ++++++++++-------- .../dialogs/CreateFileDialogFragment.java | 2 +- .../fileselect/FileDatabaseHistoryAdapter.kt | 9 +- 5 files changed, 67 insertions(+), 65 deletions(-) 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 100f7ba71..1601201e6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -471,7 +471,7 @@ class FileDatabaseSelectActivity : StylishActivity(), return true } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 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 bfede2351..98f1dfc42 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -743,7 +743,7 @@ class PasswordActivity : StylishActivity(), override fun onActivityResult( requestCode: Int, resultCode: Int, - data: Intent) { + data: Intent?) { super.onActivityResult(requestCode, resultCode, data) // To get entry in result diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/dialogs/AssignMasterKeyDialogFragment.kt index 8334b0ca8..490eab2d6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/AssignMasterKeyDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/dialogs/AssignMasterKeyDialogFragment.kt @@ -40,19 +40,19 @@ import com.kunzisoft.keepass.utils.UriUtil class AssignMasterKeyDialogFragment : DialogFragment() { - private var masterPassword: String? = null - private var mKeyfile: Uri? = null + private var mMasterPassword: String? = null + private var mKeyFile: Uri? = null private var rootView: View? = null private var passwordCheckBox: CompoundButton? = null private var passView: TextView? = null private var passConfView: TextView? = null - private var keyfileCheckBox: CompoundButton? = null - private var keyfileView: TextView? = null + private var keyFileCheckBox: CompoundButton? = null + private var keyFileView: TextView? = null private var mListener: AssignPasswordDialogListener? = null - private var keyFileHelper: KeyFileHelper? = null + private var mKeyFileHelper: KeyFileHelper? = null interface AssignPasswordDialogListener { fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?, @@ -67,7 +67,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { try { mListener = activity as AssignPasswordDialogListener? } catch (e: ClassCastException) { - throw ClassCastException(activity!!.toString() + throw ClassCastException(activity?.toString() + " must implement " + AssignPasswordDialogListener::class.java.name) } @@ -99,51 +99,51 @@ class AssignMasterKeyDialogFragment : DialogFragment() { }) passConfView = rootView?.findViewById(R.id.pass_conf_password) - keyfileCheckBox = rootView?.findViewById(R.id.keyfile_checkox) - keyfileView = rootView?.findViewById(R.id.pass_keyfile) - keyfileView?.addTextChangedListener(object : TextWatcher { + keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox) + keyFileView = rootView?.findViewById(R.id.pass_keyfile) + keyFileView?.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun afterTextChanged(editable: Editable) { - keyfileCheckBox?.isChecked = true + keyFileCheckBox?.isChecked = true } }) - keyFileHelper = KeyFileHelper(this) + mKeyFileHelper = KeyFileHelper(this) rootView?.findViewById(R.id.browse_button)?.setOnClickListener { view -> - keyFileHelper?.openFileOnClickViewListener?.onClick(view) } + mKeyFileHelper?.openFileOnClickViewListener?.onClick(view) } val dialog = builder.create() - if (passwordCheckBox != null && keyfileCheckBox!= null) { + if (passwordCheckBox != null && keyFileCheckBox!= null) { dialog.setOnShowListener { dialog1 -> val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE) positiveButton.setOnClickListener { - masterPassword = "" - mKeyfile = null + mMasterPassword = "" + mKeyFile = null var error = verifyPassword() || verifyFile() - if (!passwordCheckBox!!.isChecked && !keyfileCheckBox!!.isChecked) { + if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) { error = true showNoKeyConfirmationDialog() } if (!error) { mListener!!.onAssignKeyDialogPositiveClick( - passwordCheckBox!!.isChecked, masterPassword, - keyfileCheckBox!!.isChecked, mKeyfile) + passwordCheckBox!!.isChecked, mMasterPassword, + keyFileCheckBox!!.isChecked, mKeyFile) dismiss() } } val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE) negativeButton.setOnClickListener { - mListener!!.onAssignKeyDialogNegativeClick( - passwordCheckBox!!.isChecked, masterPassword, - keyfileCheckBox!!.isChecked, mKeyfile) + mListener?.onAssignKeyDialogNegativeClick( + passwordCheckBox!!.isChecked, mMasterPassword, + keyFileCheckBox!!.isChecked, mKeyFile) dismiss() } } @@ -157,18 +157,21 @@ class AssignMasterKeyDialogFragment : DialogFragment() { private fun verifyPassword(): Boolean { var error = false - if (passwordCheckBox!!.isChecked) { - masterPassword = passView!!.text.toString() - val confpass = passConfView!!.text.toString() + if (passwordCheckBox != null + && passwordCheckBox!!.isChecked + && passView != null + && passConfView != null) { + mMasterPassword = passView!!.text.toString() + val confPassword = passConfView!!.text.toString() // Verify that passwords match - if (masterPassword != confpass) { + if (mMasterPassword != confPassword) { error = true // Passwords do not match Toast.makeText(context, R.string.error_pass_match, Toast.LENGTH_LONG).show() } - if (masterPassword == null || masterPassword!!.isEmpty()) { + if (mMasterPassword == null || mMasterPassword!!.isEmpty()) { error = true showEmptyPasswordConfirmationDialog() } @@ -178,12 +181,14 @@ class AssignMasterKeyDialogFragment : DialogFragment() { private fun verifyFile(): Boolean { var error = false - if (keyfileCheckBox!!.isChecked) { - val keyfile = UriUtil.parseDefaultFile(keyfileView!!.text.toString()) - mKeyfile = keyfile + if (keyFileCheckBox != null + && keyFileCheckBox!!.isChecked + && keyFileView != null) { + val keyFile = UriUtil.parseDefaultFile(keyFileView!!.text.toString()) + mKeyFile = keyFile // Verify that a keyfile is set - if (EmptyUtils.isNullOrEmpty(keyfile)) { + if (EmptyUtils.isNullOrEmpty(keyFile)) { error = true Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show() } @@ -192,43 +197,47 @@ class AssignMasterKeyDialogFragment : DialogFragment() { } private fun showEmptyPasswordConfirmationDialog() { - val builder = AlertDialog.Builder(activity!!) - builder.setMessage(R.string.warning_empty_password) - .setPositiveButton(android.R.string.ok) { _, _ -> - if (!verifyFile()) { - mListener!!.onAssignKeyDialogPositiveClick( - passwordCheckBox!!.isChecked, masterPassword, - keyfileCheckBox!!.isChecked, mKeyfile) - this@AssignMasterKeyDialogFragment.dismiss() + activity?.let { + val builder = AlertDialog.Builder(it) + builder.setMessage(R.string.warning_empty_password) + .setPositiveButton(android.R.string.ok) { _, _ -> + if (!verifyFile()) { + mListener?.onAssignKeyDialogPositiveClick( + passwordCheckBox!!.isChecked, mMasterPassword, + keyFileCheckBox!!.isChecked, mKeyFile) + this@AssignMasterKeyDialogFragment.dismiss() + } } - } - .setNegativeButton(R.string.cancel) { _, _ -> } - builder.create().show() + .setNegativeButton(R.string.cancel) { _, _ -> } + builder.create().show() + } } private fun showNoKeyConfirmationDialog() { - val builder = AlertDialog.Builder(activity!!) - builder.setMessage(R.string.warning_no_encryption_key) - .setPositiveButton(android.R.string.ok) { _, _ -> - mListener!!.onAssignKeyDialogPositiveClick( - passwordCheckBox!!.isChecked, masterPassword, - keyfileCheckBox!!.isChecked, mKeyfile) - this@AssignMasterKeyDialogFragment.dismiss() - } - .setNegativeButton(R.string.cancel) { _, _ -> } - builder.create().show() + activity?.let { + val builder = AlertDialog.Builder(it) + builder.setMessage(R.string.warning_no_encryption_key) + .setPositiveButton(android.R.string.ok) { _, _ -> + mListener?.onAssignKeyDialogPositiveClick( + passwordCheckBox!!.isChecked, mMasterPassword, + keyFileCheckBox!!.isChecked, mKeyFile) + this@AssignMasterKeyDialogFragment.dismiss() + } + .setNegativeButton(R.string.cancel) { _, _ -> } + builder.create().show() + } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - keyFileHelper!!.onActivityResultCallback(requestCode, resultCode, data + mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data ) { uri -> if (uri != null) { val pathString = UriUtil.parseDefaultFile(uri.toString()) if (pathString != null) { - keyfileCheckBox!!.isChecked = true - keyfileView!!.text = pathString.toString() + keyFileCheckBox?.isChecked = true + keyFileView?.text = pathString.toString() } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/CreateFileDialogFragment.java b/app/src/main/java/com/kunzisoft/keepass/dialogs/CreateFileDialogFragment.java index 36e241faf..fb75ead1a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/CreateFileDialogFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/dialogs/CreateFileDialogFragment.java @@ -187,7 +187,7 @@ public class CreateFileDialogFragment extends DialogFragment implements AdapterV @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == FILE_CODE && resultCode == Activity.RESULT_OK) { + if (data != null && requestCode == FILE_CODE && resultCode == Activity.RESULT_OK) { uriPath = data.getData(); if (uriPath != null) { File file = Utils.getFileForUri(uriPath); diff --git a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.kt index c2e89f4f0..c24235a37 100644 --- a/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/fileselect/FileDatabaseHistoryAdapter.kt @@ -20,20 +20,13 @@ package com.kunzisoft.keepass.fileselect import android.content.Context -import android.content.res.Resources import android.net.Uri import android.support.annotation.ColorInt import android.support.v7.widget.RecyclerView import android.util.TypedValue -import android.view.ContextMenu -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.view.* import android.widget.ImageView import android.widget.TextView - import com.kunzisoft.keepass.R import com.kunzisoft.keepass.settings.PreferencesUtil From d937ca85a228818a421aec219f962bca637a860f Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 10 Jul 2019 23:37:40 +0200 Subject: [PATCH 18/24] Kotlinized fragments --- .../keepass/activities/EntryActivity.kt | 2 +- .../keepass/activities/EntryEditActivity.kt | 24 +- .../activities/FileDatabaseSelectActivity.kt | 10 +- .../keepass/activities/GroupActivity.kt | 110 ++++----- .../keepass/activities/ListNodesFragment.kt | 2 +- .../keepass/activities/PasswordActivity.kt | 2 +- .../dialogs/AssignMasterKeyDialogFragment.kt | 14 +- .../dialogs/CreateFileDialogFragment.kt | 211 +++++++++++++++++ .../dialogs/GeneratePasswordDialogFragment.kt | 188 +++++++++++++++ .../dialogs/GroupEditDialogFragment.kt | 199 ++++++++++++++++ .../dialogs/IconPickerDialogFragment.kt | 138 +++++++++++ .../KeyboardExplanationDialogFragment.kt | 67 ++++++ .../dialogs/PasswordEncodingDialogHelper.kt | 2 +- .../dialogs/ProFeatureDialogFragment.kt | 75 ++++++ .../activities/dialogs/ReadOnlyDialog.kt | 51 +++++ .../activities/dialogs/SortDialogFragment.kt | 189 +++++++++++++++ .../UnavailableFeatureDialogFragment.kt | 125 ++++++++++ .../UnderDevelopmentFeatureDialogFragment.kt | 85 +++++++ .../view/AddNodeButtonView.java | 6 +- .../view/EntryContentsView.kt | 2 +- .../view/EntryCustomField.java | 2 +- .../view/EntryCustomFieldProtected.java | 2 +- .../view/EntryEditCustomField.java | 2 +- .../dialogs/CreateFileDialogFragment.java | 216 ------------------ .../GeneratePasswordDialogFragment.java | 214 ----------------- .../dialogs/GroupEditDialogFragment.java | 216 ------------------ .../dialogs/IconPickerDialogFragment.java | 152 ------------ .../KeyboardExplanationDialogFragment.java | 75 ------ .../dialogs/ProFeatureDialogFragment.java | 74 ------ .../keepass/dialogs/ReadOnlyDialog.java | 38 --- .../keepass/dialogs/SortDialogFragment.java | 213 ----------------- .../UnavailableFeatureDialogFragment.java | 143 ------------ ...UnderDevelopmentFeatureDialogFragment.java | 85 ------- .../keepass/dialogs/WarningDialog.java | 78 ------- .../settings/NestedSettingsFragment.java | 12 +- app/src/main/res/layout/entry_view.xml | 3 +- .../res/layout/list_nodes_with_add_button.xml | 2 +- 37 files changed, 1423 insertions(+), 1606 deletions(-) rename app/src/main/java/com/kunzisoft/keepass/{ => activities}/dialogs/AssignMasterKeyDialogFragment.kt (97%) create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/dialogs/CreateFileDialogFragment.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/dialogs/KeyboardExplanationDialogFragment.kt rename app/src/main/java/com/kunzisoft/keepass/{ => activities}/dialogs/PasswordEncodingDialogHelper.kt (96%) create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ProFeatureDialogFragment.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ReadOnlyDialog.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/dialogs/UnavailableFeatureDialogFragment.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/dialogs/UnderDevelopmentFeatureDialogFragment.kt rename app/src/main/java/com/kunzisoft/keepass/{ => activities}/view/AddNodeButtonView.java (99%) rename app/src/main/java/com/kunzisoft/keepass/{ => activities}/view/EntryContentsView.kt (99%) rename app/src/main/java/com/kunzisoft/keepass/{ => activities}/view/EntryCustomField.java (98%) rename app/src/main/java/com/kunzisoft/keepass/{ => activities}/view/EntryCustomFieldProtected.java (97%) rename app/src/main/java/com/kunzisoft/keepass/{ => activities}/view/EntryEditCustomField.java (98%) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/CreateFileDialogFragment.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/GeneratePasswordDialogFragment.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/GroupEditDialogFragment.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/IconPickerDialogFragment.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/KeyboardExplanationDialogFragment.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/ProFeatureDialogFragment.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/ReadOnlyDialog.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/SortDialogFragment.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/UnavailableFeatureDialogFragment.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/UnderDevelopmentFeatureDialogFragment.java delete mode 100644 app/src/main/java/com/kunzisoft/keepass/dialogs/WarningDialog.java 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 2d582cfe9..c61f82f62 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -36,6 +36,7 @@ import android.widget.TextView import android.widget.Toast import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.lock.LockingHideActivity +import com.kunzisoft.keepass.activities.view.EntryContentsView import com.kunzisoft.keepass.app.App import com.kunzisoft.keepass.database.element.EntryVersioned import com.kunzisoft.keepass.database.element.PwNodeId @@ -48,7 +49,6 @@ 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.view.EntryContentsView class EntryActivity : LockingHideActivity() { 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 92b042499..0b363cd28 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -29,38 +29,26 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.EditText -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.Toast - +import android.widget.* import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment +import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment +import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment.Companion.KEY_ICON_STANDARD import com.kunzisoft.keepass.activities.lock.LockingHideActivity +import com.kunzisoft.keepass.activities.view.EntryEditCustomField import com.kunzisoft.keepass.app.App import com.kunzisoft.keepass.database.action.node.ActionNodeValues import com.kunzisoft.keepass.database.action.node.AddEntryRunnable import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.EntryVersioned -import com.kunzisoft.keepass.database.element.GroupVersioned -import com.kunzisoft.keepass.database.element.PwDate -import com.kunzisoft.keepass.database.element.PwIcon -import com.kunzisoft.keepass.database.element.PwIconStandard -import com.kunzisoft.keepass.database.element.PwNodeId +import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.security.ProtectedString -import com.kunzisoft.keepass.dialogs.GeneratePasswordDialogFragment -import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.Util -import com.kunzisoft.keepass.view.EntryEditCustomField - -import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment.KEY_ICON_STANDARD class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener { 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 1601201e6..40fbcbec4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -47,8 +47,8 @@ import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable import com.kunzisoft.keepass.database.action.ProgressDialogRunnable -import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment -import com.kunzisoft.keepass.dialogs.CreateFileDialogFragment +import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment +import com.kunzisoft.keepass.activities.dialogs.CreateFileDialogFragment import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.fileselect.* import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory @@ -376,8 +376,10 @@ class FileDatabaseSelectActivity : StylishActivity(), } - override fun onDefinePathDialogPositiveClick(pathFile: Uri): Boolean { + override fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean { mDatabaseFileUri = pathFile + if (pathFile == null) + return false return if (createDatabaseFile(pathFile)) { AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog") true @@ -385,7 +387,7 @@ class FileDatabaseSelectActivity : StylishActivity(), false } - override fun onDefinePathDialogNegativeClick(pathFile: Uri): Boolean { + override fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean { return true } 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 f985e8d2c..ec1067b49 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -42,6 +42,7 @@ import android.view.MenuItem import android.view.View import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.lock.LockingActivity @@ -62,12 +63,12 @@ import com.kunzisoft.keepass.database.action.node.MoveEntryRunnable import com.kunzisoft.keepass.database.action.node.MoveGroupRunnable import com.kunzisoft.keepass.database.action.node.UpdateGroupRunnable import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment -import com.kunzisoft.keepass.dialogs.GroupEditDialogFragment -import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment -import com.kunzisoft.keepass.dialogs.PasswordEncodingDialogHelper -import com.kunzisoft.keepass.dialogs.ReadOnlyDialog -import com.kunzisoft.keepass.dialogs.SortDialogFragment +import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment +import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment +import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment +import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogHelper +import com.kunzisoft.keepass.activities.dialogs.ReadOnlyDialog +import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.magikeyboard.KeyboardEntryNotificationService import com.kunzisoft.keepass.magikeyboard.KeyboardHelper @@ -77,7 +78,7 @@ import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.MenuUtil -import com.kunzisoft.keepass.view.AddNodeButtonView +import com.kunzisoft.keepass.activities.view.AddNodeButtonView import net.cachapa.expandablelayout.ExpandableLayout @@ -774,54 +775,59 @@ class GroupActivity : LockingActivity(), AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog") } - override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction, - name: String, - icon: PwIcon) { + override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?, + name: String?, + icon: PwIcon?) { val database = App.currentDatabase - when (action) { - GroupEditDialogFragment.EditGroupDialogAction.CREATION -> { - // If group creation - mCurrentGroup?.let { currentGroup -> - // Build the group - database.createGroup()?.let { newGroup-> - newGroup.title = name - newGroup.icon = icon - // Not really needed here because added in runnable but safe - newGroup.parent = currentGroup + if (name.isNullOrEmpty() || icon == null) + Toast.makeText(this, R.string.error_no_name, Toast.LENGTH_LONG).show() + else { + when (action) { + GroupEditDialogFragment.EditGroupDialogAction.CREATION -> { + // If group creation + mCurrentGroup?.let { currentGroup -> + // Build the group + database.createGroup()?.let { newGroup -> + newGroup.title = name + newGroup.icon = icon + // Not really needed here because added in runnable but safe + newGroup.parent = currentGroup - // If group created save it in the database - Thread(AddGroupRunnable(this, - App.currentDatabase, - newGroup, - currentGroup, - AfterAddNodeRunnable(), - !readOnly) - ).start() + // If group created save it in the database + Thread(AddGroupRunnable(this, + App.currentDatabase, + newGroup, + currentGroup, + AfterAddNodeRunnable(), + !readOnly) + ).start() + } } } + GroupEditDialogFragment.EditGroupDialogAction.UPDATE -> + // If update add new elements + mOldGroupToUpdate?.let { oldGroupToUpdate -> + GroupVersioned(oldGroupToUpdate).let { updateGroup -> + updateGroup.title = name + // TODO custom icon + updateGroup.icon = icon + + listNodesFragment?.removeNode(oldGroupToUpdate) + + // If group updated save it in the database + Thread(UpdateGroupRunnable(this, + App.currentDatabase, + oldGroupToUpdate, + updateGroup, + AfterUpdateNodeRunnable(), + !readOnly) + ).start() + } + } + else -> { + } } - GroupEditDialogFragment.EditGroupDialogAction.UPDATE -> - // If update add new elements - mOldGroupToUpdate?.let { oldGroupToUpdate -> - GroupVersioned(oldGroupToUpdate).let { updateGroup -> - updateGroup.title = name - // TODO custom icon - updateGroup.icon = icon - - listNodesFragment?.removeNode(oldGroupToUpdate) - - // If group updated save it in the database - Thread(UpdateGroupRunnable(this, - App.currentDatabase, - oldGroupToUpdate, - updateGroup, - AfterUpdateNodeRunnable(), - !readOnly) - ).start() - } - } - else -> {} } } @@ -874,9 +880,9 @@ class GroupActivity : LockingActivity(), } } - override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction, - name: String, - iconId: PwIcon) { + override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?, + name: String?, + iconId: PwIcon?) { // Do nothing here } 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 7e8b00b5d..01f1096da 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt @@ -20,7 +20,7 @@ import com.kunzisoft.keepass.adapters.NodeAdapter import com.kunzisoft.keepass.database.SortNodeEnum import com.kunzisoft.keepass.database.element.GroupVersioned import com.kunzisoft.keepass.database.element.NodeVersioned -import com.kunzisoft.keepass.dialogs.SortDialogFragment +import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.activities.stylish.StylishFragment 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 98f1dfc42..fbc9f6ab1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -52,7 +52,7 @@ import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable import com.kunzisoft.keepass.database.action.ProgressDialogRunnable import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.dialogs.PasswordEncodingDialogHelper +import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogHelper import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.fileselect.KeyFileHelper import com.kunzisoft.keepass.fingerprint.FingerPrintAnimatedVector diff --git a/app/src/main/java/com/kunzisoft/keepass/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt similarity index 97% rename from app/src/main/java/com/kunzisoft/keepass/dialogs/AssignMasterKeyDialogFragment.kt rename to app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt index 490eab2d6..c94a52980 100644 --- a/app/src/main/java/com/kunzisoft/keepass/dialogs/AssignMasterKeyDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt @@ -17,7 +17,7 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.dialogs +package com.kunzisoft.keepass.activities.dialogs import android.app.Dialog import android.content.Context @@ -57,7 +57,6 @@ class AssignMasterKeyDialogFragment : DialogFragment() { interface AssignPasswordDialogListener { fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?, keyFileChecked: Boolean, keyFile: Uri?) - fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?, keyFileChecked: Boolean, keyFile: Uri?) } @@ -74,10 +73,9 @@ class AssignMasterKeyDialogFragment : DialogFragment() { } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - - activity?.let { notNullActivity -> - val builder = AlertDialog.Builder(notNullActivity) - val inflater = notNullActivity.layoutInflater + activity?.let { activity -> + val builder = AlertDialog.Builder(activity) + val inflater = activity.layoutInflater rootView = inflater.inflate(R.layout.set_password, null) builder.setView(rootView) @@ -126,14 +124,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() { mKeyFile = null var error = verifyPassword() || verifyFile() - if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) { error = true showNoKeyConfirmationDialog() } - if (!error) { - mListener!!.onAssignKeyDialogPositiveClick( + mListener?.onAssignKeyDialogPositiveClick( passwordCheckBox!!.isChecked, mMasterPassword, keyFileCheckBox!!.isChecked, mKeyFile) dismiss() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/CreateFileDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/CreateFileDialogFragment.kt new file mode 100644 index 000000000..4984a98d1 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/CreateFileDialogFragment.kt @@ -0,0 +1,211 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.activities.dialogs + +import android.app.Activity +import android.app.Dialog +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Environment +import android.support.v4.app.DialogFragment +import android.support.v7.app.AlertDialog +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.EditText +import android.widget.Spinner +import android.widget.TextView + +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.stylish.FilePickerStylishActivity +import com.kunzisoft.keepass.utils.UriUtil +import com.nononsenseapps.filepicker.FilePickerActivity +import com.nononsenseapps.filepicker.Utils + +class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedListener { + + private val FILE_CODE = 3853 + + private var folderPathView: EditText? = null + private var fileNameView: EditText? = null + private var positiveButton: Button? = null + private var negativeButton: Button? = null + + private var mDefinePathDialogListener: DefinePathDialogListener? = null + + private var mDatabaseFileExtension: String? = null + private var mUriPath: Uri? = null + + interface DefinePathDialogListener { + fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean + fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean + } + + override fun onAttach(activity: Context?) { + super.onAttach(activity) + try { + mDefinePathDialogListener = activity as DefinePathDialogListener? + } catch (e: ClassCastException) { + throw ClassCastException(activity?.toString() + + " must implement " + DefinePathDialogListener::class.java.name) + } + + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + activity?.let { activity -> + val builder = AlertDialog.Builder(activity) + val inflater = activity.layoutInflater + + val rootView = inflater.inflate(R.layout.file_creation, null) + builder.setView(rootView) + .setTitle(R.string.create_keepass_file) + // Add action buttons + .setPositiveButton(android.R.string.ok) { _, _ -> } + .setNegativeButton(R.string.cancel) { _, _ -> } + + // To prevent crash issue #69 https://github.com/Kunzisoft/KeePassDX/issues/69 + val actionCopyBarCallback = object : ActionMode.Callback { + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + positiveButton?.isEnabled = false + negativeButton?.isEnabled = false + return true + } + + override fun onDestroyActionMode(mode: ActionMode) { + positiveButton?.isEnabled = true + negativeButton?.isEnabled = true + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + return true + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + return true + } + } + + // Folder selection + val browseView = rootView.findViewById(R.id.browse_button) + folderPathView = rootView.findViewById(R.id.folder_path) + folderPathView?.customSelectionActionModeCallback = actionCopyBarCallback + fileNameView = rootView.findViewById(R.id.filename) + fileNameView?.customSelectionActionModeCallback = actionCopyBarCallback + + val defaultPath = Environment.getExternalStorageDirectory().path + getString(R.string.database_file_path_default) + folderPathView?.setText(defaultPath) + browseView.setOnClickListener { _ -> + Intent(context, FilePickerStylishActivity::class.java).apply { + putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) + putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) + putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) + putExtra(FilePickerActivity.EXTRA_START_PATH, + Environment.getExternalStorageDirectory().path) + startActivityForResult(this, FILE_CODE) + } + } + + // Init path + mUriPath = null + + // Extension + mDatabaseFileExtension = getString(R.string.database_file_extension_default) + val spinner = rootView.findViewById(R.id.file_types) + spinner.onItemSelectedListener = this + + // Spinner Drop down elements + val fileTypes = resources.getStringArray(R.array.file_types) + val dataAdapter = ArrayAdapter(activity!!, android.R.layout.simple_spinner_item, fileTypes) + dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + spinner.adapter = dataAdapter + // Or text if only one item https://github.com/Kunzisoft/KeePassDX/issues/105 + if (fileTypes.size == 1) { + val params = spinner.layoutParams + spinner.visibility = View.GONE + val extensionTextView = TextView(context) + extensionTextView.text = mDatabaseFileExtension + extensionTextView.layoutParams = params + val parentView = spinner.parent as ViewGroup + parentView.addView(extensionTextView) + } + + val dialog = builder.create() + + dialog.setOnShowListener { dialog1 -> + positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE) + negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE) + positiveButton?.setOnClickListener { _ -> + mDefinePathDialogListener?.let { + if (it.onDefinePathDialogPositiveClick(buildPath())) + dismiss() + } + } + negativeButton?.setOnClickListener { _-> + mDefinePathDialogListener?.let { + if (it.onDefinePathDialogNegativeClick(buildPath())) { + dismiss() + } + } + } + } + return dialog + } + return super.onCreateDialog(savedInstanceState) + } + + private fun buildPath(): Uri? { + if (folderPathView != null && mDatabaseFileExtension != null) { + var path = Uri.Builder().path(folderPathView!!.text.toString()) + .appendPath(fileNameView!!.text.toString() + mDatabaseFileExtension!!) + .build() + path = UriUtil.translate(context, path) + return path + } + return null + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == FILE_CODE && resultCode == Activity.RESULT_OK) { + mUriPath = data?.data + mUriPath?.let { + val file = Utils.getFileForUri(it) + folderPathView?.setText(file.path) + } + } + } + + override fun onItemSelected(adapterView: AdapterView<*>, view: View, position: Int, id: Long) { + mDatabaseFileExtension = adapterView.getItemAtPosition(position).toString() + } + + override fun onNothingSelected(adapterView: AdapterView<*>) { + // Do nothing + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt new file mode 100644 index 000000000..0c7b6a4a8 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.activities.dialogs + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.support.v7.app.AlertDialog +import android.view.View +import android.widget.* +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.password.PasswordGenerator +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.utils.Util + +class GeneratePasswordDialogFragment : DialogFragment() { + + private var mListener: GeneratePasswordListener? = null + + private var root: View? = null + private var lengthTextView: EditText? = null + private var passwordView: EditText? = null + + private var uppercaseBox: CompoundButton? = null + private var lowercaseBox: CompoundButton? = null + private var digitsBox: CompoundButton? = null + private var minusBox: CompoundButton? = null + private var underlineBox: CompoundButton? = null + private var spaceBox: CompoundButton? = null + private var specialsBox: CompoundButton? = null + private var bracketsBox: CompoundButton? = null + private var extendedBox: CompoundButton? = null + + override fun onAttach(context: Context?) { + super.onAttach(context) + try { + mListener = context as GeneratePasswordListener? + } catch (e: ClassCastException) { + throw ClassCastException(context?.toString() + + " must implement " + GeneratePasswordListener::class.java.name) + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + activity?.let { activity -> + val builder = AlertDialog.Builder(activity) + val inflater = activity.layoutInflater + root = inflater.inflate(R.layout.generate_password, null) + + passwordView = root?.findViewById(R.id.password) + Util.applyFontVisibilityTo(context, passwordView) + + lengthTextView = root?.findViewById(R.id.length) + + uppercaseBox = root?.findViewById(R.id.cb_uppercase) + lowercaseBox = root?.findViewById(R.id.cb_lowercase) + digitsBox = root?.findViewById(R.id.cb_digits) + minusBox = root?.findViewById(R.id.cb_minus) + underlineBox = root?.findViewById(R.id.cb_underline) + spaceBox = root?.findViewById(R.id.cb_space) + specialsBox = root?.findViewById(R.id.cb_specials) + bracketsBox = root?.findViewById(R.id.cb_brackets) + extendedBox = root?.findViewById(R.id.cb_extended) + + assignDefaultCharacters() + + val seekBar = root?.findViewById(R.id.seekbar_length) + seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + lengthTextView?.setText(progress.toString()) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) {} + + override fun onStopTrackingTouch(seekBar: SeekBar) {} + }) + seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context) + + root?.findViewById